vendor: 27,531 bytes, lines 1-989
1/** 2 * @license 3 * Video.js 7.0.5 <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.0.5"; 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 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
vendor: 21,809 bytes, lines 1313-2093
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 { 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; 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: 50,457 bytes, lines 2762-4373
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-using-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 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 fn = bind(this, fn); 4318 4319 var timeoutId = window_1.setTimeout(fn, timeout); 4320 var disposeFn = function disposeFn() { 4321 return _this2.clearTimeout(timeoutId); 4322 }; 4323 4324 disposeFn.guid = 'vjs-timeout-' + timeoutId; 4325 4326 this.on('dispose', disposeFn); 4327 4328 return timeoutId; 4329 }; 4330 4331 /** 4332 * Clears a timeout that gets created via `window.setTimeout` or 4333 * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} 4334 * use this function instead of `window.clearTimout`. If you don't your dispose 4335 * listener will not get cleaned up until {@link Component#dispose}! 4336 * 4337 * @param {number} timeoutId 4338 * The id of the timeout to clear. The return value of 4339 * {@link Component#setTimeout} or `window.setTimeout`. 4340 * 4341 * @return {number} 4342 * Returns the timeout id that was cleared. 4343 * 4344 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} 4345 */ 4346 4347 4348 Component.prototype.clearTimeout = function clearTimeout(timeoutId) { 4349 window_1.clearTimeout(timeoutId); 4350 4351 var disposeFn = function disposeFn() {}; 4352 4353 disposeFn.guid = 'vjs-timeout-' + timeoutId; 4354 4355 this.off('dispose', disposeFn); 4356 4357 return timeoutId; 4358 }; 4359 4360 /** 4361 * Creates a function that gets run every `x` milliseconds. This function is a wrapper 4362 * around `window.setInterval`. There are a few reasons to use this one instead though. 4363 * 1. It gets cleared via {@link Component#clearInterval} when 4364 * {@link Component#dispose} gets called. 4365 * 2. The function callback will be a {@link Component~GenericCallback} 4366 * 4367 * @param {Component~GenericCallback} fn 4368 * The function to run every `x` seconds. 4369 * 4370 * @param {number} interval 4371 * Execute the specified function every `x` milliseconds. 4372 * 4373 * @return {number}
4374 * Returns an id that can be used to identify the interval. It can also be be used in 4375 * {@link Component#clearInterval} to clear the interval. 4376 * 4377 * @listens Component#dispose 4378 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} 4379 */ 4380 4381 4382 Component.prototype.setInterval = function setInterval(fn, interval) { 4383 var _this3 = this; 4384 4385 fn = bind(this, fn); 4386 4387 var intervalId = window_1.setInterval(fn, interval); 4388 4389 var disposeFn = function disposeFn() { 4390 return _this3.clearInterval(intervalId); 4391 }; 4392 4393 disposeFn.guid = 'vjs-interval-' + intervalId; 4394 4395 this.on('dispose', disposeFn); 4396 4397 return intervalId; 4398 }; 4399 4400 /** 4401 * Clears an interval that gets created via `window.setInterval` or 4402 * {@link Component#setInterval}. If you set an inteval via {@link Component#setInterval} 4403 * use this function instead of `window.clearInterval`. If you don't your dispose 4404 * listener will not get cleaned up until {@link Component#dispose}! 4405 * 4406 * @param {number} intervalId 4407 * The id of the interval to clear. The return value of 4408 * {@link Component#setInterval} or `window.setInterval`. 4409 * 4410 * @return {number} 4411 * Returns the interval id that was cleared. 4412 * 4413 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} 4414 */ 4415 4416 4417 Component.prototype.clearInterval = function clearInterval(intervalId) { 4418 window_1.clearInterval(intervalId); 4419 4420 var disposeFn = function disposeFn() {}; 4421 4422 disposeFn.guid = 'vjs-interval-' + intervalId; 4423 4424 this.off('dispose', disposeFn); 4425 4426 return intervalId; 4427 }; 4428 4429 /** 4430 * Queues up a callback to be passed to requestAnimationFrame (rAF), but 4431 * with a few extra bonuses: 4432 * 4433 * - Supports browsers that do not support rAF by falling back to 4434 * {@link Component#setTimeout}. 4435 * 4436 * - The callback is turned into a {@link Component~GenericCallback} (i.e. 4437 * bound to the component). 4438 * 4439 * - Automatic cancellation of the rAF callback is handled if the component 4440 * is disposed before it is called. 4441 * 4442 * @param {Component~GenericCallback} fn 4443 * A function that will be bound to this component and executed just 4444 * before the browser's next repaint. 4445 * 4446 * @return {number} 4447 * Returns an rAF ID that gets used to identify the timeout. It can 4448 * also be used in {@link Component#cancelAnimationFrame} to cancel 4449 * the animation frame callback. 4450 * 4451 * @listens Component#dispose 4452 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} 4453 */ 4454 4455 4456 Component.prototype.requestAnimationFrame = function requestAnimationFrame(fn) { 4457 var _this4 = this; 4458 4459 if (this.supportsRaf_) { 4460 fn = bind(this, fn); 4461 4462 var id = window_1.requestAnimationFrame(fn); 4463 var disposeFn = function disposeFn() { 4464 return _this4.cancelAnimationFrame(id); 4465 }; 4466 4467 disposeFn.guid = 'vjs-raf-' + id; 4468 this.on('dispose', disposeFn); 4469 4470 return id; 4471 } 4472 4473 // Fall back to using a timer. 4474 return this.setTimeout(fn, 1000 / 60); 4475 }; 4476 4477 /** 4478 * Cancels a queued callback passed to {@link Component#requestAnimationFrame} 4479 * (rAF). 4480 * 4481 * If you queue an rAF callback via {@link Component#requestAnimationFrame}, 4482 * use this function instead of `window.cancelAnimationFrame`. If you don't, 4483 * your dispose listener will not get cleaned up until {@link Component#dispose}! 4484 * 4485 * @param {number} id 4486 * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. 4487 * 4488 * @return {number} 4489 * Returns the rAF ID that was cleared. 4490 * 4491 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} 4492 */ 4493 4494 4495 Component.prototype.cancelAnimationFrame = function cancelAnimationFrame(id) { 4496 if (this.supportsRaf_) { 4497 window_1.cancelAnimationFrame(id); 4498 4499 var disposeFn = function disposeFn() {}; 4500 4501 disposeFn.guid = 'vjs-raf-' + id; 4502 4503 this.off('dispose', disposeFn); 4504 4505 return id; 4506 } 4507 4508 // Fall back to using a timer. 4509 return this.clearTimeout(id); 4510 }; 4511 4512 /** 4513 * Register a `Component` with `videojs` given the name and the component. 4514 * 4515 * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s 4516 * should be registered using {@link Tech.registerTech} or
vendor: 13,279 bytes, lines 4517-4957
4517 * {@link videojs:videojs.registerTech}. 4518 * 4519 * > NOTE: This function can also be seen on videojs as 4520 * {@link videojs:videojs.registerComponent}. 4521 * 4522 * @param {string} name 4523 * The name of the `Component` to register. 4524 * 4525 * @param {Component} ComponentToRegister 4526 * The `Component` class to register. 4527 * 4528 * @return {Component} 4529 * The `Component` that was registered. 4530 */ 4531 4532 4533 Component.registerComponent = function registerComponent(name, ComponentToRegister) { 4534 if (typeof name !== 'string' || !name) { 4535 throw new Error('Illegal component name, "' + name + '"; must be a non-empty string.'); 4536 } 4537 4538 var Tech = Component.getComponent('Tech'); 4539 4540 // We need to make sure this check is only done if Tech has been registered. 4541 var isTech = Tech && Tech.isTech(ComponentToRegister); 4542 var isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype); 4543 4544 if (isTech || !isComp) { 4545 var reason = void 0; 4546 4547 if (isTech) { 4548 reason = 'techs must be registered using Tech.registerTech()'; 4549 } else { 4550 reason = 'must be a Component subclass'; 4551 } 4552 4553 throw new Error('Illegal component, "' + name + '"; ' + reason + '.'); 4554 } 4555 4556 name = toTitleCase(name); 4557 4558 if (!Component.components_) { 4559 Component.components_ = {}; 4560 } 4561 4562 var Player = Component.getComponent('Player'); 4563 4564 if (name === 'Player' && Player && Player.players) { 4565 var players = Player.players; 4566 var playerNames = Object.keys(players); 4567 4568 // If we have players that were disposed, then their name will still be 4569 // in Players.players. So, we must loop through and verify that the value 4570 // for each item is not null. This allows registration of the Player component 4571 // after all players have been disposed or before any were created. 4572 if (players && playerNames.length > 0 && playerNames.map(function (pname) { 4573 return players[pname]; 4574 }).every(Boolean)) { 4575 throw new Error('Can not register Player component after player has been created.'); 4576 } 4577 } 4578 4579 Component.components_[name] = ComponentToRegister; 4580 4581 return ComponentToRegister; 4582 }; 4583 4584 /** 4585 * Get a `Component` based on the name it was registered with. 4586 * 4587 * @param {string} name 4588 * The Name of the component to get. 4589 * 4590 * @return {Component} 4591 * The `Component` that got registered under the given name. 4592 * 4593 * @deprecated In `videojs` 6 this will not return `Component`s that were not 4594 * registered using {@link Component.registerComponent}. Currently we 4595 * check the global `videojs` object for a `Component` name and 4596 * return that if it exists. 4597 */ 4598 4599 4600 Component.getComponent = function getComponent(name) { 4601 if (!name) { 4602 return; 4603 } 4604 4605 name = toTitleCase(name); 4606 4607 if (Component.components_ && Component.components_[name]) { 4608 return Component.components_[name]; 4609 } 4610 }; 4611 4612 return Component; 4613 }(); 4614 4615 /** 4616 * Whether or not this component supports `requestAnimationFrame`. 4617 * 4618 * This is exposed primarily for testing purposes. 4619 * 4620 * @private 4621 * @type {Boolean} 4622 */ 4623 4624 4625 Component.prototype.supportsRaf_ = typeof window_1.requestAnimationFrame === 'function' && typeof window_1.cancelAnimationFrame === 'function'; 4626 4627 Component.registerComponent('Component', Component); 4628 4629 /** 4630 * @file browser.js 4631 * @module browser 4632 */ 4633 4634 var USER_AGENT = window_1.navigator && window_1.navigator.userAgent || ''; 4635 var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); 4636 var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; 4637 4638 /* 4639 * Device is an iPhone 4640 * 4641 * @type {Boolean} 4642 * @constant 4643 * @private 4644 */ 4645 var IS_IPAD = /iPad/i.test(USER_AGENT); 4646 4647 // The Facebook app's UIWebView identifies as both an iPhone and iPad, so 4648 // to identify iPhones, we need to exclude iPads. 4649 // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ 4650 var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; 4651 var IS_IPOD = /iPod/i.test(USER_AGENT); 4652 var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; 4653 4654 var IOS_VERSION = function () { 4655 var match = USER_AGENT.match(/OS (\d+)_/i); 4656 4657 if (match && match[1]) { 4658 return match[1]; 4659 } 4660 return null; 4661 }(); 4662 4663 var IS_ANDROID = /Android/i.test(USER_AGENT); 4664 var ANDROID_VERSION = function () { 4665 // This matches Android Major.Minor.Patch versions 4666 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 4667 var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); 4668 4669 if (!match) { 4670 return null; 4671 } 4672 4673 var major = match[1] && parseFloat(match[1]); 4674 var minor = match[2] && parseFloat(match[2]); 4675 4676 if (major && minor) { 4677 return parseFloat(match[1] + '.' + match[2]); 4678 } else if (major) { 4679 return major; 4680 } 4681 return null; 4682 }(); 4683 4684 var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; 4685 4686 var IS_FIREFOX = /Firefox/i.test(USER_AGENT); 4687 var IS_EDGE = /Edge/i.test(USER_AGENT); 4688 var IS_CHROME = !IS_EDGE && /Chrome/i.test(USER_AGENT); 4689 var CHROME_VERSION = function () { 4690 var match = USER_AGENT.match(/Chrome\/(\d+)/); 4691 4692 if (match && match[1]) { 4693 return parseFloat(match[1]); 4694 } 4695 return null; 4696 }(); 4697 var IE_VERSION = function () { 4698 var result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); 4699 var version = result && parseFloat(result[1]); 4700 4701 if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { 4702 // IE 11 has a different user agent string than other IE versions 4703 version = 11.0; 4704 } 4705 4706 return version; 4707 }(); 4708 4709 var IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; 4710 var IS_ANY_SAFARI = IS_SAFARI || IS_IOS; 4711 4712 var TOUCH_ENABLED = isReal() && ('ontouchstart' in window_1 || window_1.DocumentTouch && window_1.document instanceof window_1.DocumentTouch); 4713 4714 var browser = /*#__PURE__*/Object.freeze({ 4715 IS_IPAD: IS_IPAD, 4716 IS_IPHONE: IS_IPHONE, 4717 IS_IPOD: IS_IPOD, 4718 IS_IOS: IS_IOS, 4719 IOS_VERSION: IOS_VERSION, 4720 IS_ANDROID: IS_ANDROID, 4721 ANDROID_VERSION: ANDROID_VERSION, 4722 IS_NATIVE_ANDROID: IS_NATIVE_ANDROID, 4723 IS_FIREFOX: IS_FIREFOX, 4724 IS_EDGE: IS_EDGE, 4725 IS_CHROME: IS_CHROME, 4726 CHROME_VERSION: CHROME_VERSION, 4727 IE_VERSION: IE_VERSION, 4728 IS_SAFARI: IS_SAFARI, 4729 IS_ANY_SAFARI: IS_ANY_SAFARI, 4730 TOUCH_ENABLED: TOUCH_ENABLED 4731 }); 4732 4733 /** 4734 * @file time-ranges.js 4735 * @module time-ranges 4736 */ 4737 4738 /** 4739 * Returns the time for the specified index at the start or end 4740 * of a TimeRange object. 4741 * 4742 * @function time-ranges:indexFunction 4743 * 4744 * @param {number} [index=0] 4745 * The range number to return the time for. 4746 * 4747 * @return {number} 4748 * The time that offset at the specified index. 4749 * 4750 * @depricated index must be set to a value, in the future this will throw an error. 4751 */ 4752 4753 /** 4754 * An object that contains ranges of time for various reasons. 4755 * 4756 * @typedef {Object} TimeRange 4757 * 4758 * @property {number} length 4759 * The number of time ranges represented by this Object 4760 * 4761 * @property {time-ranges:indexFunction} start 4762 * Returns the time offset at which a specified time range begins. 4763 * 4764 * @property {time-ranges:indexFunction} end 4765 * Returns the time offset at which a specified time range ends. 4766 * 4767 * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges 4768 */ 4769 4770 /** 4771 * Check if any of the time ranges are over the maximum index. 4772 * 4773 * @param {string} fnName 4774 * The function name to use for logging 4775 * 4776 * @param {number} index 4777 * The index to check 4778 * 4779 * @param {number} maxIndex 4780 * The maximum possible index 4781 * 4782 * @throws {Error} if the timeRanges provided are over the maxIndex 4783 */ 4784 function rangeCheck(fnName, index, maxIndex) { 4785 if (typeof index !== 'number' || index < 0 || index > maxIndex) { 4786 throw new Error('Failed to execute \'' + fnName + '\' on \'TimeRanges\': The index provided (' + index + ') is non-numeric or out of bounds (0-' + maxIndex + ').'); 4787 } 4788 } 4789 4790 /** 4791 * Get the time for the specified index at the start or end 4792 * of a TimeRange object. 4793 * 4794 * @param {string} fnName 4795 * The function name to use for logging 4796 * 4797 * @param {string} valueIndex 4798 * The property that should be used to get the time. should be 'start' or 'end' 4799 * 4800 * @param {Array} ranges 4801 * An array of time ranges 4802 * 4803 * @param {Array} [rangeIndex=0] 4804 * The index to start the search at 4805 * 4806 * @return {number} 4807 * The time that offset at the specified index. 4808 * 4809 * 4810 * @depricated rangeIndex must be set to a value, in the future this will throw an error. 4811 * @throws {Error} if rangeIndex is more than the length of ranges 4812 */ 4813 function getRange(fnName, valueIndex, ranges, rangeIndex) { 4814 rangeCheck(fnName, rangeIndex, ranges.length - 1); 4815 return ranges[rangeIndex][valueIndex]; 4816 } 4817 4818 /** 4819 * Create a time range object given ranges of time. 4820 * 4821 * @param {Array} [ranges] 4822 * An array of time ranges. 4823 */ 4824 function createTimeRangesObj(ranges) { 4825 if (ranges === undefined || ranges.length === 0) { 4826 return { 4827 length: 0, 4828 start: function start() { 4829 throw new Error('This TimeRanges object is empty'); 4830 }, 4831 end: function end() { 4832 throw new Error('This TimeRanges object is empty'); 4833 } 4834 }; 4835 } 4836 return { 4837 length: ranges.length, 4838 start: getRange.bind(null, 'start', 0, ranges), 4839 end: getRange.bind(null, 'end', 1, ranges) 4840 }; 4841 } 4842 4843 /** 4844 * Should create a fake `TimeRange` object which mimics an HTML5 time range instance. 4845 * 4846 * @param {number|Array} start 4847 * The start of a single range or an array of ranges 4848 * 4849 * @param {number} end 4850 * The end of a single range. 4851 * 4852 * @private 4853 */ 4854 function createTimeRanges(start, end) { 4855 if (Array.isArray(start)) { 4856 return createTimeRangesObj(start); 4857 } else if (start === undefined || end === undefined) { 4858 return createTimeRangesObj(); 4859 } 4860 return createTimeRangesObj([[start, end]]); 4861 } 4862 4863 /** 4864 * @file buffer.js 4865 * @module buffer 4866 */ 4867 4868 /** 4869 * Compute the percentage of the media that has been buffered. 4870 * 4871 * @param {TimeRange} buffered 4872 * The current `TimeRange` object representing buffered time ranges 4873 * 4874 * @param {number} duration 4875 * Total duration of the media 4876 * 4877 * @return {number} 4878 * Percent buffered of the total duration in decimal form. 4879 */ 4880 function bufferedPercent(buffered, duration) { 4881 var bufferedDuration = 0; 4882 var start = void 0; 4883 var end = void 0; 4884 4885 if (!duration) { 4886 return 0; 4887 } 4888 4889 if (!buffered || !buffered.length) { 4890 buffered = createTimeRanges(0, 0); 4891 } 4892 4893 for (var i = 0; i < buffered.length; i++) { 4894 start = buffered.start(i); 4895 end = buffered.end(i); 4896 4897 // buffered end can be bigger than duration by a very small fraction 4898 if (end > duration) { 4899 end = duration; 4900 } 4901 4902 bufferedDuration += end - start; 4903 } 4904 4905 return bufferedDuration / duration; 4906 } 4907 4908 /** 4909 * @file fullscreen-api.js 4910 * @module fullscreen-api 4911 * @private 4912 */ 4913 4914 /** 4915 * Store the browser-specific methods for the fullscreen API. 4916 * 4917 * @type {Object} 4918 * @see [Specification]{@link https://fullscreen.spec.whatwg.org} 4919 * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} 4920 */ 4921 var FullscreenApi = {}; 4922 4923 // browser API methods 4924 var apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror'], 4925 // WebKit 4926 ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror'], 4927 // Old WebKit (Safari 5.1) 4928 ['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror'], 4929 // Mozilla 4930 ['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror'], 4931 // Microsoft 4932 ['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError']]; 4933 4934 var specApi = apiMap[0]; 4935 var browserApi = void 0; 4936 4937 // determine the supported set of functions 4938 for (var i = 0; i < apiMap.length; i++) { 4939 // check for exitFullscreen function 4940 if (apiMap[i][1] in document_1) { 4941 browserApi = apiMap[i]; 4942 break; 4943 } 4944 } 4945 4946 // map the browser API names to the spec API names 4947 if (browserApi) { 4948 for (var _i = 0; _i < browserApi.length; _i++) { 4949 FullscreenApi[specApi[_i]] = browserApi[_i]; 4950 } 4951 } 4952 4953 /** 4954 * @file media-error.js 4955 */ 4956 4957 /**
4958 * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. 4959 * 4960 * @param {number|string|Object|MediaError} value 4961 * This can be of multiple types: 4962 * - number: should be a standard error code 4963 * - string: an error message (the code will be 0) 4964 * - Object: arbitrary properties 4965 * - `MediaError` (native): used to populate a video.js `MediaError` object 4966 * - `MediaError` (video.js): will return itself if it's already a 4967 * video.js `MediaError` object. 4968 * 4969 * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} 4970 * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} 4971 * 4972 * @class MediaError 4973 */ 4974 function MediaError(value) { 4975 4976 // Allow redundant calls to this constructor to avoid having `instanceof` 4977 // checks peppered around the code. 4978 if (value instanceof MediaError) { 4979 return value; 4980 } 4981 4982 if (typeof value === 'number') { 4983 this.code = value; 4984 } else if (typeof value === 'string') {
vendor: 49,953 bytes, lines 4985-6820
4985 // default code is zero, so this is a custom error 4986 this.message = value; 4987 } else if (isObject(value)) { 4988 4989 // We assign the `code` property manually because native `MediaError` objects 4990 // do not expose it as an own/enumerable property of the object. 4991 if (typeof value.code === 'number') { 4992 this.code = value.code; 4993 } 4994 4995 assign(this, value); 4996 } 4997 4998 if (!this.message) { 4999 this.message = MediaError.defaultMessages[this.code] || ''; 5000 } 5001 } 5002 5003 /** 5004 * The error code that refers two one of the defined `MediaError` types 5005 * 5006 * @type {Number} 5007 */ 5008 MediaError.prototype.code = 0; 5009 5010 /** 5011 * An optional message that to show with the error. Message is not part of the HTML5 5012 * video spec but allows for more informative custom errors. 5013 * 5014 * @type {String} 5015 */ 5016 MediaError.prototype.message = ''; 5017 5018 /** 5019 * An optional status code that can be set by plugins to allow even more detail about 5020 * the error. For example a plugin might provide a specific HTTP status code and an 5021 * error message for that code. Then when the plugin gets that error this class will 5022 * know how to display an error message for it. This allows a custom message to show 5023 * up on the `Player` error overlay. 5024 * 5025 * @type {Array} 5026 */ 5027 MediaError.prototype.status = null; 5028 5029 /** 5030 * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the 5031 * specification listed under {@link MediaError} for more information. 5032 * 5033 * @enum {array} 5034 * @readonly 5035 * @property {string} 0 - MEDIA_ERR_CUSTOM 5036 * @property {string} 1 - MEDIA_ERR_CUSTOM 5037 * @property {string} 2 - MEDIA_ERR_ABORTED 5038 * @property {string} 3 - MEDIA_ERR_NETWORK 5039 * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED 5040 * @property {string} 5 - MEDIA_ERR_ENCRYPTED 5041 */ 5042 MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED']; 5043 5044 /** 5045 * The default `MediaError` messages based on the {@link MediaError.errorTypes}. 5046 * 5047 * @type {Array} 5048 * @constant 5049 */ 5050 MediaError.defaultMessages = { 5051 1: 'You aborted the media playback', 5052 2: 'A network error caused the media download to fail part-way.', 5053 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', 5054 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', 5055 5: 'The media is encrypted and we do not have the keys to decrypt it.' 5056 }; 5057 5058 // Add types as properties on MediaError 5059 // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 5060 for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { 5061 MediaError[MediaError.errorTypes[errNum]] = errNum; 5062 // values should be accessible on both the class and instance 5063 MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; 5064 } 5065 5066 var tuple = SafeParseTuple; 5067 5068 function SafeParseTuple(obj, reviver) { 5069 var json; 5070 var error = null; 5071 5072 try { 5073 json = JSON.parse(obj, reviver); 5074 } catch (err) { 5075 error = err; 5076 } 5077 5078 return [error, json]; 5079 } 5080 5081 /** 5082 * Returns whether an object is `Promise`-like (i.e. has a `then` method). 5083 * 5084 * @param {Object} value 5085 * An object that may or may not be `Promise`-like. 5086 * 5087 * @return {Boolean} 5088 * Whether or not the object is `Promise`-like. 5089 */ 5090 function isPromise(value) { 5091 return value !== undefined && value !== null && typeof value.then === 'function'; 5092 } 5093 5094 /** 5095 * Silence a Promise-like object. 5096 * 5097 * This is useful for avoiding non-harmful, but potentially confusing "uncaught 5098 * play promise" rejection error messages. 5099 * 5100 * @param {Object} value 5101 * An object that may or may not be `Promise`-like. 5102 */ 5103 function silencePromise(value) { 5104 if (isPromise(value)) { 5105 value.then(null, function (e) {}); 5106 } 5107 } 5108 5109 /** 5110 * @file text-track-list-converter.js Utilities for capturing text track state and 5111 * re-creating tracks based on a capture. 5112 * 5113 * @module text-track-list-converter 5114 */ 5115 5116 /** 5117 * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that 5118 * represents the {@link TextTrack}'s state. 5119 * 5120 * @param {TextTrack} track 5121 * The text track to query. 5122 * 5123 * @return {Object} 5124 * A serializable javascript representation of the TextTrack. 5125 * @private 5126 */ 5127 var trackToJson_ = function trackToJson_(track) { 5128 var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (acc, prop, i) { 5129 5130 if (track[prop]) { 5131 acc[prop] = track[prop]; 5132 } 5133 5134 return acc; 5135 }, { 5136 cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { 5137 return { 5138 startTime: cue.startTime, 5139 endTime: cue.endTime, 5140 text: cue.text, 5141 id: cue.id 5142 }; 5143 }) 5144 }); 5145 5146 return ret; 5147 }; 5148 5149 /** 5150 * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the 5151 * state of all {@link TextTrack}s currently configured. The return array is compatible with 5152 * {@link text-track-list-converter:jsonToTextTracks}. 5153 * 5154 * @param {Tech} tech 5155 * The tech object to query 5156 * 5157 * @return {Array} 5158 * A serializable javascript representation of the {@link Tech}s 5159 * {@link TextTrackList}. 5160 */ 5161 var textTracksToJson = function textTracksToJson(tech) { 5162 5163 var trackEls = tech.$$('track'); 5164 5165 var trackObjs = Array.prototype.map.call(trackEls, function (t) { 5166 return t.track; 5167 }); 5168 var tracks = Array.prototype.map.call(trackEls, function (trackEl) { 5169 var json = trackToJson_(trackEl.track); 5170 5171 if (trackEl.src) { 5172 json.src = trackEl.src; 5173 } 5174 return json; 5175 }); 5176 5177 return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { 5178 return trackObjs.indexOf(track) === -1; 5179 }).map(trackToJson_)); 5180 }; 5181 5182 /** 5183 * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript 5184 * object {@link TextTrack} representations. 5185 * 5186 * @param {Array} json 5187 * An array of `TextTrack` representation objects, like those that would be 5188 * produced by `textTracksToJson`. 5189 * 5190 * @param {Tech} tech 5191 * The `Tech` to create the `TextTrack`s on. 5192 */ 5193 var jsonToTextTracks = function jsonToTextTracks(json, tech) { 5194 json.forEach(function (track) { 5195 var addedTrack = tech.addRemoteTextTrack(track).track; 5196 5197 if (!track.src && track.cues) { 5198 track.cues.forEach(function (cue) { 5199 return addedTrack.addCue(cue); 5200 }); 5201 } 5202 }); 5203 5204 return tech.textTracks(); 5205 }; 5206 5207 var textTrackConverter = { textTracksToJson: textTracksToJson, jsonToTextTracks: jsonToTextTracks, trackToJson_: trackToJson_ }; 5208 5209 /** 5210 * @file modal-dialog.js 5211 */ 5212 5213 var MODAL_CLASS_NAME = 'vjs-modal-dialog'; 5214 var ESC = 27; 5215 5216 /** 5217 * The `ModalDialog` displays over the video and its controls, which blocks 5218 * interaction with the player until it is closed. 5219 * 5220 * Modal dialogs include a "Close" button and will close when that button 5221 * is activated - or when ESC is pressed anywhere. 5222 * 5223 * @extends Component 5224 */ 5225 5226 var ModalDialog = function (_Component) { 5227 inherits(ModalDialog, _Component); 5228 5229 /** 5230 * Create an instance of this class. 5231 * 5232 * @param {Player} player 5233 * The `Player` that this class should be attached to. 5234 * 5235 * @param {Object} [options] 5236 * The key/value store of player options. 5237 * 5238 * @param {Mixed} [options.content=undefined] 5239 * Provide customized content for this modal. 5240 * 5241 * @param {string} [options.description] 5242 * A text description for the modal, primarily for accessibility. 5243 * 5244 * @param {boolean} [options.fillAlways=false] 5245 * Normally, modals are automatically filled only the first time 5246 * they open. This tells the modal to refresh its content 5247 * every time it opens. 5248 * 5249 * @param {string} [options.label] 5250 * A text label for the modal, primarily for accessibility. 5251 * 5252 * @param {boolean} [options.temporary=true] 5253 * If `true`, the modal can only be opened once; it will be 5254 * disposed as soon as it's closed. 5255 * 5256 * @param {boolean} [options.uncloseable=false] 5257 * If `true`, the user will not be able to close the modal 5258 * through the UI in the normal ways. Programmatic closing is 5259 * still possible. 5260 */ 5261 function ModalDialog(player, options) { 5262 classCallCheck(this, ModalDialog); 5263 5264 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 5265 5266 _this.opened_ = _this.hasBeenOpened_ = _this.hasBeenFilled_ = false; 5267 5268 _this.closeable(!_this.options_.uncloseable); 5269 _this.content(_this.options_.content); 5270 5271 // Make sure the contentEl is defined AFTER any children are initialized 5272 // because we only want the contents of the modal in the contentEl 5273 // (not the UI elements like the close button). 5274 _this.contentEl_ = createEl('div', { 5275 className: MODAL_CLASS_NAME + '-content' 5276 }, { 5277 role: 'document' 5278 }); 5279 5280 _this.descEl_ = createEl('p', { 5281 className: MODAL_CLASS_NAME + '-description vjs-control-text', 5282 id: _this.el().getAttribute('aria-describedby') 5283 }); 5284 5285 textContent(_this.descEl_, _this.description()); 5286 _this.el_.appendChild(_this.descEl_); 5287 _this.el_.appendChild(_this.contentEl_); 5288 return _this; 5289 } 5290 5291 /** 5292 * Create the `ModalDialog`'s DOM element 5293 * 5294 * @return {Element} 5295 * The DOM element that gets created. 5296 */ 5297 5298 5299 ModalDialog.prototype.createEl = function createEl$$1() { 5300 return _Component.prototype.createEl.call(this, 'div', { 5301 className: this.buildCSSClass(), 5302 tabIndex: -1 5303 }, { 5304 'aria-describedby': this.id() + '_description', 5305 'aria-hidden': 'true', 5306 'aria-label': this.label(), 5307 'role': 'dialog' 5308 }); 5309 }; 5310 5311 ModalDialog.prototype.dispose = function dispose() { 5312 this.contentEl_ = null; 5313 this.descEl_ = null; 5314 this.previouslyActiveEl_ = null; 5315 5316 _Component.prototype.dispose.call(this); 5317 }; 5318 5319 /** 5320 * Builds the default DOM `className`. 5321 * 5322 * @return {string} 5323 * The DOM `className` for this object. 5324 */ 5325 5326 5327 ModalDialog.prototype.buildCSSClass = function buildCSSClass() { 5328 return MODAL_CLASS_NAME + ' vjs-hidden ' + _Component.prototype.buildCSSClass.call(this); 5329 }; 5330 5331 /** 5332 * Handles `keydown` events on the document, looking for ESC, which closes 5333 * the modal. 5334 * 5335 * @param {EventTarget~Event} e 5336 * The keypress that triggered this event. 5337 * 5338 * @listens keydown 5339 */ 5340 5341 5342 ModalDialog.prototype.handleKeyPress = function handleKeyPress(e) { 5343 if (e.which === ESC && this.closeable()) { 5344 this.close(); 5345 } 5346 }; 5347 5348 /** 5349 * Returns the label string for this modal. Primarily used for accessibility. 5350 * 5351 * @return {string} 5352 * the localized or raw label of this modal. 5353 */ 5354 5355 5356 ModalDialog.prototype.label = function label() { 5357 return this.localize(this.options_.label || 'Modal Window'); 5358 }; 5359 5360 /** 5361 * Returns the description string for this modal. Primarily used for 5362 * accessibility. 5363 * 5364 * @return {string} 5365 * The localized or raw description of this modal. 5366 */ 5367 5368 5369 ModalDialog.prototype.description = function description() { 5370 var desc = this.options_.description || this.localize('This is a modal window.'); 5371 5372 // Append a universal closeability message if the modal is closeable. 5373 if (this.closeable()) { 5374 desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); 5375 } 5376 5377 return desc; 5378 }; 5379 5380 /** 5381 * Opens the modal. 5382 * 5383 * @fires ModalDialog#beforemodalopen 5384 * @fires ModalDialog#modalopen 5385 */ 5386 5387 5388 ModalDialog.prototype.open = function open() { 5389 if (!this.opened_) { 5390 var player = this.player(); 5391 5392 /** 5393 * Fired just before a `ModalDialog` is opened. 5394 * 5395 * @event ModalDialog#beforemodalopen 5396 * @type {EventTarget~Event} 5397 */ 5398 this.trigger('beforemodalopen'); 5399 this.opened_ = true; 5400 5401 // Fill content if the modal has never opened before and 5402 // never been filled. 5403 if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { 5404 this.fill(); 5405 } 5406 5407 // If the player was playing, pause it and take note of its previously 5408 // playing state. 5409 this.wasPlaying_ = !player.paused(); 5410 5411 if (this.options_.pauseOnOpen && this.wasPlaying_) { 5412 player.pause(); 5413 } 5414 5415 if (this.closeable()) { 5416 this.on(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); 5417 } 5418 5419 // Hide controls and note if they were enabled. 5420 this.hadControls_ = player.controls(); 5421 player.controls(false); 5422 5423 this.show(); 5424 this.conditionalFocus_(); 5425 this.el().setAttribute('aria-hidden', 'false'); 5426 5427 /** 5428 * Fired just after a `ModalDialog` is opened. 5429 * 5430 * @event ModalDialog#modalopen 5431 * @type {EventTarget~Event} 5432 */ 5433 this.trigger('modalopen'); 5434 this.hasBeenOpened_ = true; 5435 } 5436 }; 5437 5438 /** 5439 * If the `ModalDialog` is currently open or closed. 5440 * 5441 * @param {boolean} [value] 5442 * If given, it will open (`true`) or close (`false`) the modal. 5443 * 5444 * @return {boolean} 5445 * the current open state of the modaldialog 5446 */ 5447 5448 5449 ModalDialog.prototype.opened = function opened(value) { 5450 if (typeof value === 'boolean') { 5451 this[value ? 'open' : 'close'](); 5452 } 5453 return this.opened_; 5454 }; 5455 5456 /** 5457 * Closes the modal, does nothing if the `ModalDialog` is 5458 * not open. 5459 * 5460 * @fires ModalDialog#beforemodalclose 5461 * @fires ModalDialog#modalclose 5462 */ 5463 5464 5465 ModalDialog.prototype.close = function close() { 5466 if (!this.opened_) { 5467 return; 5468 } 5469 var player = this.player(); 5470 5471 /** 5472 * Fired just before a `ModalDialog` is closed. 5473 * 5474 * @event ModalDialog#beforemodalclose 5475 * @type {EventTarget~Event} 5476 */ 5477 this.trigger('beforemodalclose'); 5478 this.opened_ = false; 5479 5480 if (this.wasPlaying_ && this.options_.pauseOnOpen) { 5481 player.play(); 5482 } 5483 5484 if (this.closeable()) { 5485 this.off(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); 5486 } 5487 5488 if (this.hadControls_) { 5489 player.controls(true); 5490 } 5491 5492 this.hide(); 5493 this.el().setAttribute('aria-hidden', 'true'); 5494 5495 /** 5496 * Fired just after a `ModalDialog` is closed. 5497 * 5498 * @event ModalDialog#modalclose 5499 * @type {EventTarget~Event} 5500 */ 5501 this.trigger('modalclose'); 5502 this.conditionalBlur_(); 5503 5504 if (this.options_.temporary) { 5505 this.dispose(); 5506 } 5507 }; 5508 5509 /** 5510 * Check to see if the `ModalDialog` is closeable via the UI. 5511 * 5512 * @param {boolean} [value] 5513 * If given as a boolean, it will set the `closeable` option. 5514 * 5515 * @return {boolean} 5516 * Returns the final value of the closable option. 5517 */ 5518 5519 5520 ModalDialog.prototype.closeable = function closeable(value) { 5521 if (typeof value === 'boolean') { 5522 var closeable = this.closeable_ = !!value; 5523 var close = this.getChild('closeButton'); 5524 5525 // If this is being made closeable and has no close button, add one. 5526 if (closeable && !close) { 5527 5528 // The close button should be a child of the modal - not its 5529 // content element, so temporarily change the content element. 5530 var temp = this.contentEl_; 5531 5532 this.contentEl_ = this.el_; 5533 close = this.addChild('closeButton', { controlText: 'Close Modal Dialog' }); 5534 this.contentEl_ = temp; 5535 this.on(close, 'close', this.close); 5536 } 5537 5538 // If this is being made uncloseable and has a close button, remove it. 5539 if (!closeable && close) { 5540 this.off(close, 'close', this.close); 5541 this.removeChild(close); 5542 close.dispose(); 5543 } 5544 } 5545 return this.closeable_; 5546 }; 5547 5548 /** 5549 * Fill the modal's content element with the modal's "content" option. 5550 * The content element will be emptied before this change takes place. 5551 */ 5552 5553 5554 ModalDialog.prototype.fill = function fill() { 5555 this.fillWith(this.content()); 5556 }; 5557 5558 /** 5559 * Fill the modal's content element with arbitrary content. 5560 * The content element will be emptied before this change takes place. 5561 * 5562 * @fires ModalDialog#beforemodalfill 5563 * @fires ModalDialog#modalfill 5564 * 5565 * @param {Mixed} [content] 5566 * The same rules apply to this as apply to the `content` option. 5567 */ 5568 5569 5570 ModalDialog.prototype.fillWith = function fillWith(content) { 5571 var contentEl = this.contentEl(); 5572 var parentEl = contentEl.parentNode; 5573 var nextSiblingEl = contentEl.nextSibling; 5574 5575 /** 5576 * Fired just before a `ModalDialog` is filled with content. 5577 * 5578 * @event ModalDialog#beforemodalfill 5579 * @type {EventTarget~Event} 5580 */ 5581 this.trigger('beforemodalfill'); 5582 this.hasBeenFilled_ = true; 5583 5584 // Detach the content element from the DOM before performing 5585 // manipulation to avoid modifying the live DOM multiple times. 5586 parentEl.removeChild(contentEl); 5587 this.empty(); 5588 insertContent(contentEl, content); 5589 /** 5590 * Fired just after a `ModalDialog` is filled with content. 5591 * 5592 * @event ModalDialog#modalfill 5593 * @type {EventTarget~Event} 5594 */ 5595 this.trigger('modalfill'); 5596 5597 // Re-inject the re-filled content element. 5598 if (nextSiblingEl) { 5599 parentEl.insertBefore(contentEl, nextSiblingEl); 5600 } else { 5601 parentEl.appendChild(contentEl); 5602 } 5603 5604 // make sure that the close button is last in the dialog DOM 5605 var closeButton = this.getChild('closeButton'); 5606 5607 if (closeButton) { 5608 parentEl.appendChild(closeButton.el_); 5609 } 5610 }; 5611 5612 /** 5613 * Empties the content element. This happens anytime the modal is filled. 5614 * 5615 * @fires ModalDialog#beforemodalempty 5616 * @fires ModalDialog#modalempty 5617 */ 5618 5619 5620 ModalDialog.prototype.empty = function empty() { 5621 /** 5622 * Fired just before a `ModalDialog` is emptied. 5623 * 5624 * @event ModalDialog#beforemodalempty 5625 * @type {EventTarget~Event} 5626 */ 5627 this.trigger('beforemodalempty'); 5628 emptyEl(this.contentEl()); 5629 5630 /** 5631 * Fired just after a `ModalDialog` is emptied. 5632 * 5633 * @event ModalDialog#modalempty 5634 * @type {EventTarget~Event} 5635 */ 5636 this.trigger('modalempty'); 5637 }; 5638 5639 /** 5640 * Gets or sets the modal content, which gets normalized before being 5641 * rendered into the DOM. 5642 * 5643 * This does not update the DOM or fill the modal, but it is called during 5644 * that process. 5645 * 5646 * @param {Mixed} [value] 5647 * If defined, sets the internal content value to be used on the 5648 * next call(s) to `fill`. This value is normalized before being 5649 * inserted. To "clear" the internal content value, pass `null`. 5650 * 5651 * @return {Mixed} 5652 * The current content of the modal dialog 5653 */ 5654 5655 5656 ModalDialog.prototype.content = function content(value) { 5657 if (typeof value !== 'undefined') { 5658 this.content_ = value; 5659 } 5660 return this.content_; 5661 }; 5662 5663 /** 5664 * conditionally focus the modal dialog if focus was previously on the player. 5665 * 5666 * @private 5667 */ 5668 5669 5670 ModalDialog.prototype.conditionalFocus_ = function conditionalFocus_() { 5671 var activeEl = document_1.activeElement; 5672 var playerEl = this.player_.el_; 5673 5674 this.previouslyActiveEl_ = null; 5675 5676 if (playerEl.contains(activeEl) || playerEl === activeEl) { 5677 this.previouslyActiveEl_ = activeEl; 5678 5679 this.focus(); 5680 5681 this.on(document_1, 'keydown', this.handleKeyDown); 5682 } 5683 }; 5684 5685 /** 5686 * conditionally blur the element and refocus the last focused element 5687 * 5688 * @private 5689 */ 5690 5691 5692 ModalDialog.prototype.conditionalBlur_ = function conditionalBlur_() { 5693 if (this.previouslyActiveEl_) { 5694 this.previouslyActiveEl_.focus(); 5695 this.previouslyActiveEl_ = null; 5696 } 5697 5698 this.off(document_1, 'keydown', this.handleKeyDown); 5699 }; 5700 5701 /** 5702 * Keydown handler. Attached when modal is focused. 5703 * 5704 * @listens keydown 5705 */ 5706 5707 5708 ModalDialog.prototype.handleKeyDown = function handleKeyDown(event) { 5709 // exit early if it isn't a tab key 5710 if (event.which !== 9) { 5711 return; 5712 } 5713 5714 var focusableEls = this.focusableEls_(); 5715 var activeEl = this.el_.querySelector(':focus'); 5716 var focusIndex = void 0; 5717 5718 for (var i = 0; i < focusableEls.length; i++) { 5719 if (activeEl === focusableEls[i]) { 5720 focusIndex = i; 5721 break; 5722 } 5723 } 5724 5725 if (document_1.activeElement === this.el_) { 5726 focusIndex = 0; 5727 } 5728 5729 if (event.shiftKey && focusIndex === 0) { 5730 focusableEls[focusableEls.length - 1].focus(); 5731 event.preventDefault(); 5732 } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { 5733 focusableEls[0].focus(); 5734 event.preventDefault(); 5735 } 5736 }; 5737 5738 /** 5739 * get all focusable elements 5740 * 5741 * @private 5742 */ 5743 5744 5745 ModalDialog.prototype.focusableEls_ = function focusableEls_() { 5746 var allChildren = this.el_.querySelectorAll('*'); 5747 5748 return Array.prototype.filter.call(allChildren, function (child) { 5749 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'); 5750 }); 5751 }; 5752 5753 return ModalDialog; 5754 }(Component); 5755 5756 /** 5757 * Default options for `ModalDialog` default options. 5758 * 5759 * @type {Object} 5760 * @private 5761 */ 5762 5763 5764 ModalDialog.prototype.options_ = { 5765 pauseOnOpen: true, 5766 temporary: true 5767 }; 5768 5769 Component.registerComponent('ModalDialog', ModalDialog); 5770 5771 /** 5772 * @file track-list.js 5773 */ 5774 5775 /** 5776 * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and 5777 * {@link VideoTrackList} 5778 * 5779 * @extends EventTarget 5780 */ 5781 5782 var TrackList = function (_EventTarget) { 5783 inherits(TrackList, _EventTarget); 5784 5785 /** 5786 * Create an instance of this class 5787 * 5788 * @param {Track[]} tracks 5789 * A list of tracks to initialize the list with. 5790 * 5791 * @abstract 5792 */ 5793 function TrackList() { 5794 var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 5795 classCallCheck(this, TrackList); 5796 5797 var _this = possibleConstructorReturn(this, _EventTarget.call(this)); 5798 5799 _this.tracks_ = []; 5800 5801 /** 5802 * @memberof TrackList 5803 * @member {number} length 5804 * The current number of `Track`s in the this Trackist. 5805 * @instance 5806 */ 5807 Object.defineProperty(_this, 'length', { 5808 get: function get$$1() { 5809 return this.tracks_.length; 5810 } 5811 }); 5812 5813 for (var i = 0; i < tracks.length; i++) { 5814 _this.addTrack(tracks[i]); 5815 } 5816 return _this; 5817 } 5818 5819 /** 5820 * Add a {@link Track} to the `TrackList` 5821 * 5822 * @param {Track} track 5823 * The audio, video, or text track to add to the list. 5824 * 5825 * @fires TrackList#addtrack 5826 */ 5827 5828 5829 TrackList.prototype.addTrack = function addTrack(track) { 5830 var index = this.tracks_.length; 5831 5832 if (!('' + index in this)) { 5833 Object.defineProperty(this, index, { 5834 get: function get$$1() { 5835 return this.tracks_[index]; 5836 } 5837 }); 5838 } 5839 5840 // Do not add duplicate tracks 5841 if (this.tracks_.indexOf(track) === -1) { 5842 this.tracks_.push(track); 5843 /** 5844 * Triggered when a track is added to a track list. 5845 * 5846 * @event TrackList#addtrack 5847 * @type {EventTarget~Event} 5848 * @property {Track} track 5849 * A reference to track that was added. 5850 */ 5851 this.trigger({ 5852 track: track, 5853 type: 'addtrack' 5854 }); 5855 } 5856 }; 5857 5858 /** 5859 * Remove a {@link Track} from the `TrackList` 5860 * 5861 * @param {Track} rtrack 5862 * The audio, video, or text track to remove from the list. 5863 * 5864 * @fires TrackList#removetrack 5865 */ 5866 5867 5868 TrackList.prototype.removeTrack = function removeTrack(rtrack) { 5869 var track = void 0; 5870 5871 for (var i = 0, l = this.length; i < l; i++) { 5872 if (this[i] === rtrack) { 5873 track = this[i]; 5874 if (track.off) { 5875 track.off(); 5876 } 5877 5878 this.tracks_.splice(i, 1); 5879 5880 break; 5881 } 5882 } 5883 5884 if (!track) { 5885 return; 5886 } 5887 5888 /** 5889 * Triggered when a track is removed from track list. 5890 * 5891 * @event TrackList#removetrack 5892 * @type {EventTarget~Event} 5893 * @property {Track} track 5894 * A reference to track that was removed. 5895 */ 5896 this.trigger({ 5897 track: track, 5898 type: 'removetrack' 5899 }); 5900 }; 5901 5902 /** 5903 * Get a Track from the TrackList by a tracks id 5904 * 5905 * @param {String} id - the id of the track to get 5906 * @method getTrackById 5907 * @return {Track} 5908 * @private 5909 */ 5910 5911 5912 TrackList.prototype.getTrackById = function getTrackById(id) { 5913 var result = null; 5914 5915 for (var i = 0, l = this.length; i < l; i++) { 5916 var track = this[i]; 5917 5918 if (track.id === id) { 5919 result = track; 5920 break; 5921 } 5922 } 5923 5924 return result; 5925 }; 5926 5927 return TrackList; 5928 }(EventTarget); 5929 5930 /** 5931 * Triggered when a different track is selected/enabled. 5932 * 5933 * @event TrackList#change 5934 * @type {EventTarget~Event} 5935 */ 5936 5937 /** 5938 * Events that can be called with on + eventName. See {@link EventHandler}. 5939 * 5940 * @property {Object} TrackList#allowedEvents_ 5941 * @private 5942 */ 5943 5944 5945 TrackList.prototype.allowedEvents_ = { 5946 change: 'change', 5947 addtrack: 'addtrack', 5948 removetrack: 'removetrack' 5949 }; 5950 5951 // emulate attribute EventHandler support to allow for feature detection 5952 for (var event in TrackList.prototype.allowedEvents_) { 5953 TrackList.prototype['on' + event] = null; 5954 } 5955 5956 /** 5957 * @file audio-track-list.js 5958 */ 5959 5960 /** 5961 * Anywhere we call this function we diverge from the spec 5962 * as we only support one enabled audiotrack at a time 5963 * 5964 * @param {AudioTrackList} list 5965 * list to work on 5966 * 5967 * @param {AudioTrack} track 5968 * The track to skip 5969 * 5970 * @private 5971 */ 5972 var disableOthers = function disableOthers(list, track) { 5973 for (var i = 0; i < list.length; i++) { 5974 if (!Object.keys(list[i]).length || track.id === list[i].id) { 5975 continue; 5976 } 5977 // another audio track is enabled, disable it 5978 list[i].enabled = false; 5979 } 5980 }; 5981 5982 /** 5983 * The current list of {@link AudioTrack} for a media file. 5984 * 5985 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} 5986 * @extends TrackList 5987 */ 5988 5989 var AudioTrackList = function (_TrackList) { 5990 inherits(AudioTrackList, _TrackList); 5991 5992 /** 5993 * Create an instance of this class. 5994 * 5995 * @param {AudioTrack[]} [tracks=[]] 5996 * A list of `AudioTrack` to instantiate the list with. 5997 */ 5998 function AudioTrackList() { 5999 var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 6000 classCallCheck(this, AudioTrackList); 6001 6002 // make sure only 1 track is enabled 6003 // sorted from last index to first index 6004 for (var i = tracks.length - 1; i >= 0; i--) { 6005 if (tracks[i].enabled) { 6006 disableOthers(tracks, tracks[i]); 6007 break; 6008 } 6009 } 6010 6011 var _this = possibleConstructorReturn(this, _TrackList.call(this, tracks)); 6012 6013 _this.changing_ = false; 6014 return _this; 6015 } 6016 6017 /** 6018 * Add an {@link AudioTrack} to the `AudioTrackList`. 6019 * 6020 * @param {AudioTrack} track 6021 * The AudioTrack to add to the list 6022 * 6023 * @fires TrackList#addtrack 6024 */ 6025 6026 6027 AudioTrackList.prototype.addTrack = function addTrack(track) { 6028 var _this2 = this; 6029 6030 if (track.enabled) { 6031 disableOthers(this, track); 6032 } 6033 6034 _TrackList.prototype.addTrack.call(this, track); 6035 // native tracks don't have this 6036 if (!track.addEventListener) { 6037 return; 6038 } 6039 6040 /** 6041 * @listens AudioTrack#enabledchange 6042 * @fires TrackList#change 6043 */ 6044 track.addEventListener('enabledchange', function () { 6045 // when we are disabling other tracks (since we don't support 6046 // more than one track at a time) we will set changing_ 6047 // to true so that we don't trigger additional change events 6048 if (_this2.changing_) { 6049 return; 6050 } 6051 _this2.changing_ = true; 6052 disableOthers(_this2, track); 6053 _this2.changing_ = false; 6054 _this2.trigger('change'); 6055 }); 6056 }; 6057 6058 return AudioTrackList; 6059 }(TrackList); 6060 6061 /** 6062 * @file video-track-list.js 6063 */ 6064 6065 /** 6066 * Un-select all other {@link VideoTrack}s that are selected. 6067 * 6068 * @param {VideoTrackList} list 6069 * list to work on 6070 * 6071 * @param {VideoTrack} track 6072 * The track to skip 6073 * 6074 * @private 6075 */ 6076 var disableOthers$1 = function disableOthers(list, track) { 6077 for (var i = 0; i < list.length; i++) { 6078 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6079 continue; 6080 } 6081 // another video track is enabled, disable it 6082 list[i].selected = false; 6083 } 6084 }; 6085 6086 /** 6087 * The current list of {@link VideoTrack} for a video. 6088 * 6089 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} 6090 * @extends TrackList 6091 */ 6092 6093 var VideoTrackList = function (_TrackList) { 6094 inherits(VideoTrackList, _TrackList); 6095 6096 /** 6097 * Create an instance of this class. 6098 * 6099 * @param {VideoTrack[]} [tracks=[]] 6100 * A list of `VideoTrack` to instantiate the list with. 6101 */ 6102 function VideoTrackList() { 6103 var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 6104 classCallCheck(this, VideoTrackList); 6105 6106 // make sure only 1 track is enabled 6107 // sorted from last index to first index 6108 for (var i = tracks.length - 1; i >= 0; i--) { 6109 if (tracks[i].selected) { 6110 disableOthers$1(tracks, tracks[i]); 6111 break; 6112 } 6113 } 6114 6115 var _this = possibleConstructorReturn(this, _TrackList.call(this, tracks)); 6116 6117 _this.changing_ = false; 6118 6119 /** 6120 * @member {number} VideoTrackList#selectedIndex 6121 * The current index of the selected {@link VideoTrack`}. 6122 */ 6123 Object.defineProperty(_this, 'selectedIndex', { 6124 get: function get$$1() { 6125 for (var _i = 0; _i < this.length; _i++) { 6126 if (this[_i].selected) { 6127 return _i; 6128 } 6129 } 6130 return -1; 6131 }, 6132 set: function set$$1() {} 6133 }); 6134 return _this; 6135 } 6136 6137 /** 6138 * Add a {@link VideoTrack} to the `VideoTrackList`. 6139 * 6140 * @param {VideoTrack} track 6141 * The VideoTrack to add to the list 6142 * 6143 * @fires TrackList#addtrack 6144 */ 6145 6146 6147 VideoTrackList.prototype.addTrack = function addTrack(track) { 6148 var _this2 = this; 6149 6150 if (track.selected) { 6151 disableOthers$1(this, track); 6152 } 6153 6154 _TrackList.prototype.addTrack.call(this, track); 6155 // native tracks don't have this 6156 if (!track.addEventListener) { 6157 return; 6158 } 6159 6160 /** 6161 * @listens VideoTrack#selectedchange 6162 * @fires TrackList#change 6163 */ 6164 track.addEventListener('selectedchange', function () { 6165 if (_this2.changing_) { 6166 return; 6167 } 6168 _this2.changing_ = true; 6169 disableOthers$1(_this2, track); 6170 _this2.changing_ = false; 6171 _this2.trigger('change'); 6172 }); 6173 }; 6174 6175 return VideoTrackList; 6176 }(TrackList); 6177 6178 /** 6179 * @file text-track-list.js 6180 */ 6181 6182 /** 6183 * The current list of {@link TextTrack} for a media file. 6184 * 6185 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} 6186 * @extends TrackList 6187 */ 6188 6189 var TextTrackList = function (_TrackList) { 6190 inherits(TextTrackList, _TrackList); 6191 6192 function TextTrackList() { 6193 classCallCheck(this, TextTrackList); 6194 return possibleConstructorReturn(this, _TrackList.apply(this, arguments)); 6195 } 6196 6197 /** 6198 * Add a {@link TextTrack} to the `TextTrackList` 6199 * 6200 * @param {TextTrack} track 6201 * The text track to add to the list. 6202 * 6203 * @fires TrackList#addtrack 6204 */ 6205 TextTrackList.prototype.addTrack = function addTrack(track) { 6206 _TrackList.prototype.addTrack.call(this, track); 6207 6208 /** 6209 * @listens TextTrack#modechange 6210 * @fires TrackList#change 6211 */ 6212 track.addEventListener('modechange', bind(this, function () { 6213 this.trigger('change'); 6214 })); 6215 6216 var nonLanguageTextTrackKind = ['metadata', 'chapters']; 6217 6218 if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { 6219 track.addEventListener('modechange', bind(this, function () { 6220 this.trigger('selectedlanguagechange'); 6221 })); 6222 } 6223 }; 6224 6225 return TextTrackList; 6226 }(TrackList); 6227 6228 /** 6229 * @file html-track-element-list.js 6230 */ 6231 6232 /** 6233 * The current list of {@link HtmlTrackElement}s. 6234 */ 6235 var HtmlTrackElementList = function () { 6236 6237 /** 6238 * Create an instance of this class. 6239 * 6240 * @param {HtmlTrackElement[]} [tracks=[]] 6241 * A list of `HtmlTrackElement` to instantiate the list with. 6242 */ 6243 function HtmlTrackElementList() { 6244 var trackElements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 6245 classCallCheck(this, HtmlTrackElementList); 6246 6247 this.trackElements_ = []; 6248 6249 /** 6250 * @memberof HtmlTrackElementList 6251 * @member {number} length 6252 * The current number of `Track`s in the this Trackist. 6253 * @instance 6254 */ 6255 Object.defineProperty(this, 'length', { 6256 get: function get$$1() { 6257 return this.trackElements_.length; 6258 } 6259 }); 6260 6261 for (var i = 0, length = trackElements.length; i < length; i++) { 6262 this.addTrackElement_(trackElements[i]); 6263 } 6264 } 6265 6266 /** 6267 * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` 6268 * 6269 * @param {HtmlTrackElement} trackElement 6270 * The track element to add to the list. 6271 * 6272 * @private 6273 */ 6274 6275 6276 HtmlTrackElementList.prototype.addTrackElement_ = function addTrackElement_(trackElement) { 6277 var index = this.trackElements_.length; 6278 6279 if (!('' + index in this)) { 6280 Object.defineProperty(this, index, { 6281 get: function get$$1() { 6282 return this.trackElements_[index]; 6283 } 6284 }); 6285 } 6286 6287 // Do not add duplicate elements 6288 if (this.trackElements_.indexOf(trackElement) === -1) { 6289 this.trackElements_.push(trackElement); 6290 } 6291 }; 6292 6293 /** 6294 * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an 6295 * {@link TextTrack}. 6296 * 6297 * @param {TextTrack} track 6298 * The track associated with a track element. 6299 * 6300 * @return {HtmlTrackElement|undefined} 6301 * The track element that was found or undefined. 6302 * 6303 * @private 6304 */ 6305 6306 6307 HtmlTrackElementList.prototype.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { 6308 var trackElement_ = void 0; 6309 6310 for (var i = 0, length = this.trackElements_.length; i < length; i++) { 6311 if (track === this.trackElements_[i].track) { 6312 trackElement_ = this.trackElements_[i]; 6313 6314 break; 6315 } 6316 } 6317 6318 return trackElement_; 6319 }; 6320 6321 /** 6322 * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` 6323 * 6324 * @param {HtmlTrackElement} trackElement 6325 * The track element to remove from the list. 6326 * 6327 * @private 6328 */ 6329 6330 6331 HtmlTrackElementList.prototype.removeTrackElement_ = function removeTrackElement_(trackElement) { 6332 for (var i = 0, length = this.trackElements_.length; i < length; i++) { 6333 if (trackElement === this.trackElements_[i]) { 6334 this.trackElements_.splice(i, 1); 6335 6336 break; 6337 } 6338 } 6339 }; 6340 6341 return HtmlTrackElementList; 6342 }(); 6343 6344 /** 6345 * @file text-track-cue-list.js 6346 */ 6347 6348 /** 6349 * @typedef {Object} TextTrackCueList~TextTrackCue 6350 * 6351 * @property {string} id 6352 * The unique id for this text track cue 6353 * 6354 * @property {number} startTime 6355 * The start time for this text track cue 6356 * 6357 * @property {number} endTime 6358 * The end time for this text track cue 6359 * 6360 * @property {boolean} pauseOnExit 6361 * Pause when the end time is reached if true. 6362 * 6363 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} 6364 */ 6365 6366 /** 6367 * A List of TextTrackCues. 6368 * 6369 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} 6370 */ 6371 var TextTrackCueList = function () { 6372 6373 /** 6374 * Create an instance of this class.. 6375 * 6376 * @param {Array} cues 6377 * A list of cues to be initialized with 6378 */ 6379 function TextTrackCueList(cues) { 6380 classCallCheck(this, TextTrackCueList); 6381 6382 TextTrackCueList.prototype.setCues_.call(this, cues); 6383 6384 /** 6385 * @memberof TextTrackCueList 6386 * @member {number} length 6387 * The current number of `TextTrackCue`s in the TextTrackCueList. 6388 * @instance 6389 */ 6390 Object.defineProperty(this, 'length', { 6391 get: function get$$1() { 6392 return this.length_; 6393 } 6394 }); 6395 } 6396 6397 /** 6398 * A setter for cues in this list. Creates getters 6399 * an an index for the cues. 6400 * 6401 * @param {Array} cues 6402 * An array of cues to set 6403 * 6404 * @private 6405 */ 6406 6407 6408 TextTrackCueList.prototype.setCues_ = function setCues_(cues) { 6409 var oldLength = this.length || 0; 6410 var i = 0; 6411 var l = cues.length; 6412 6413 this.cues_ = cues; 6414 this.length_ = cues.length; 6415 6416 var defineProp = function defineProp(index) { 6417 if (!('' + index in this)) { 6418 Object.defineProperty(this, '' + index, { 6419 get: function get$$1() { 6420 return this.cues_[index]; 6421 } 6422 }); 6423 } 6424 }; 6425 6426 if (oldLength < l) { 6427 i = oldLength; 6428 6429 for (; i < l; i++) { 6430 defineProp.call(this, i); 6431 } 6432 } 6433 }; 6434 6435 /** 6436 * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. 6437 * 6438 * @param {string} id 6439 * The id of the cue that should be searched for. 6440 * 6441 * @return {TextTrackCueList~TextTrackCue|null} 6442 * A single cue or null if none was found. 6443 */ 6444 6445 6446 TextTrackCueList.prototype.getCueById = function getCueById(id) { 6447 var result = null; 6448 6449 for (var i = 0, l = this.length; i < l; i++) { 6450 var cue = this[i]; 6451 6452 if (cue.id === id) { 6453 result = cue; 6454 break; 6455 } 6456 } 6457 6458 return result; 6459 }; 6460 6461 return TextTrackCueList; 6462 }(); 6463 6464 /** 6465 * @file track-kinds.js 6466 */ 6467 6468 /** 6469 * All possible `VideoTrackKind`s 6470 * 6471 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind 6472 * @typedef VideoTrack~Kind 6473 * @enum 6474 */ 6475 var VideoTrackKind = { 6476 alternative: 'alternative', 6477 captions: 'captions', 6478 main: 'main', 6479 sign: 'sign', 6480 subtitles: 'subtitles', 6481 commentary: 'commentary' 6482 }; 6483 6484 /** 6485 * All possible `AudioTrackKind`s 6486 * 6487 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind 6488 * @typedef AudioTrack~Kind 6489 * @enum 6490 */ 6491 var AudioTrackKind = { 6492 'alternative': 'alternative', 6493 'descriptions': 'descriptions', 6494 'main': 'main', 6495 'main-desc': 'main-desc', 6496 'translation': 'translation', 6497 'commentary': 'commentary' 6498 }; 6499 6500 /** 6501 * All possible `TextTrackKind`s 6502 * 6503 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind 6504 * @typedef TextTrack~Kind 6505 * @enum 6506 */ 6507 var TextTrackKind = { 6508 subtitles: 'subtitles', 6509 captions: 'captions', 6510 descriptions: 'descriptions', 6511 chapters: 'chapters', 6512 metadata: 'metadata' 6513 }; 6514 6515 /** 6516 * All possible `TextTrackMode`s 6517 * 6518 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode 6519 * @typedef TextTrack~Mode 6520 * @enum 6521 */ 6522 var TextTrackMode = { 6523 disabled: 'disabled', 6524 hidden: 'hidden', 6525 showing: 'showing' 6526 }; 6527 6528 /** 6529 * @file track.js 6530 */ 6531 6532 /** 6533 * A Track class that contains all of the common functionality for {@link AudioTrack}, 6534 * {@link VideoTrack}, and {@link TextTrack}. 6535 * 6536 * > Note: This class should not be used directly 6537 * 6538 * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html} 6539 * @extends EventTarget 6540 * @abstract 6541 */ 6542 6543 var Track = function (_EventTarget) { 6544 inherits(Track, _EventTarget); 6545 6546 /** 6547 * Create an instance of this class. 6548 * 6549 * @param {Object} [options={}] 6550 * Object of option names and values 6551 * 6552 * @param {string} [options.kind=''] 6553 * A valid kind for the track type you are creating. 6554 * 6555 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 6556 * A unique id for this AudioTrack. 6557 * 6558 * @param {string} [options.label=''] 6559 * The menu label for this track. 6560 * 6561 * @param {string} [options.language=''] 6562 * A valid two character language code. 6563 * 6564 * @abstract 6565 */ 6566 function Track() { 6567 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 6568 classCallCheck(this, Track); 6569 6570 var _this = possibleConstructorReturn(this, _EventTarget.call(this)); 6571 6572 var trackProps = { 6573 id: options.id || 'vjs_track_' + newGUID(), 6574 kind: options.kind || '', 6575 label: options.label || '', 6576 language: options.language || '' 6577 }; 6578 6579 /** 6580 * @memberof Track 6581 * @member {string} id 6582 * The id of this track. Cannot be changed after creation. 6583 * @instance 6584 * 6585 * @readonly 6586 */ 6587 6588 /** 6589 * @memberof Track 6590 * @member {string} kind 6591 * The kind of track that this is. Cannot be changed after creation. 6592 * @instance 6593 * 6594 * @readonly 6595 */ 6596 6597 /** 6598 * @memberof Track 6599 * @member {string} label 6600 * The label of this track. Cannot be changed after creation. 6601 * @instance 6602 * 6603 * @readonly 6604 */ 6605 6606 /** 6607 * @memberof Track 6608 * @member {string} language 6609 * The two letter language code for this track. Cannot be changed after 6610 * creation. 6611 * @instance 6612 * 6613 * @readonly 6614 */ 6615 6616 var _loop = function _loop(key) { 6617 Object.defineProperty(_this, key, { 6618 get: function get$$1() { 6619 return trackProps[key]; 6620 }, 6621 set: function set$$1() {} 6622 }); 6623 }; 6624 6625 for (var key in trackProps) { 6626 _loop(key); 6627 } 6628 return _this; 6629 } 6630 6631 return Track; 6632 }(EventTarget); 6633 6634 /** 6635 * @file url.js 6636 * @module url 6637 */ 6638 6639 /** 6640 * @typedef {Object} url:URLObject 6641 * 6642 * @property {string} protocol 6643 * The protocol of the url that was parsed. 6644 * 6645 * @property {string} hostname 6646 * The hostname of the url that was parsed. 6647 * 6648 * @property {string} port 6649 * The port of the url that was parsed. 6650 * 6651 * @property {string} pathname 6652 * The pathname of the url that was parsed. 6653 * 6654 * @property {string} search 6655 * The search query of the url that was parsed. 6656 * 6657 * @property {string} hash 6658 * The hash of the url that was parsed. 6659 * 6660 * @property {string} host 6661 * The host of the url that was parsed. 6662 */ 6663 6664 /** 6665 * Resolve and parse the elements of a URL. 6666 * 6667 * @param {String} url 6668 * The url to parse 6669 * 6670 * @return {url:URLObject} 6671 * An object of url details 6672 */ 6673 var parseUrl = function parseUrl(url) { 6674 var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; 6675 6676 // add the url to an anchor and let the browser parse the URL 6677 var a = document_1.createElement('a'); 6678 6679 a.href = url; 6680 6681 // IE8 (and 9?) Fix 6682 // ie8 doesn't parse the URL correctly until the anchor is actually 6683 // added to the body, and an innerHTML is needed to trigger the parsing 6684 var addToBody = a.host === '' && a.protocol !== 'file:'; 6685 var div = void 0; 6686 6687 if (addToBody) { 6688 div = document_1.createElement('div'); 6689 div.innerHTML = '<a href="' + url + '"></a>'; 6690 a = div.firstChild; 6691 // prevent the div from affecting layout 6692 div.setAttribute('style', 'display:none; position:absolute;'); 6693 document_1.body.appendChild(div); 6694 } 6695 6696 // Copy the specific URL properties to a new object 6697 // This is also needed for IE8 because the anchor loses its 6698 // properties when it's removed from the dom 6699 var details = {}; 6700 6701 for (var i = 0; i < props.length; i++) { 6702 details[props[i]] = a[props[i]]; 6703 } 6704 6705 // IE9 adds the port to the host property unlike everyone else. If 6706 // a port identifier is added for standard ports, strip it. 6707 if (details.protocol === 'http:') { 6708 details.host = details.host.replace(/:80$/, ''); 6709 } 6710 6711 if (details.protocol === 'https:') { 6712 details.host = details.host.replace(/:443$/, ''); 6713 } 6714 6715 if (!details.protocol) { 6716 details.protocol = window_1.location.protocol; 6717 } 6718 6719 if (addToBody) { 6720 document_1.body.removeChild(div); 6721 } 6722 6723 return details; 6724 }; 6725 6726 /** 6727 * Get absolute version of relative URL. Used to tell flash correct URL. 6728 * 6729 * 6730 * @param {string} url 6731 * URL to make absolute 6732 * 6733 * @return {string} 6734 * Absolute URL 6735 * 6736 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 6737 */ 6738 var getAbsoluteURL = function getAbsoluteURL(url) { 6739 // Check if absolute URL 6740 if (!url.match(/^https?:\/\//)) { 6741 // Convert to absolute URL. Flash hosted off-site needs an absolute URL. 6742 var div = document_1.createElement('div'); 6743 6744 div.innerHTML = '<a href="' + url + '">x</a>'; 6745 url = div.firstChild.href; 6746 } 6747 6748 return url; 6749 }; 6750 6751 /** 6752 * Returns the extension of the passed file name. It will return an empty string 6753 * if passed an invalid path. 6754 * 6755 * @param {string} path 6756 * The fileName path like '/path/to/file.mp4' 6757 * 6758 * @returns {string} 6759 * The extension in lower case or an empty string if no 6760 * extension could be found. 6761 */ 6762 var getFileExtension = function getFileExtension(path) { 6763 if (typeof path === 'string') { 6764 var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i; 6765 var pathParts = splitPathRe.exec(path); 6766 6767 if (pathParts) { 6768 return pathParts.pop().toLowerCase(); 6769 } 6770 } 6771 6772 return ''; 6773 }; 6774 6775 /** 6776 * Returns whether the url passed is a cross domain request or not. 6777 * 6778 * @param {string} url 6779 * The url to check. 6780 * 6781 * @return {boolean} 6782 * Whether it is a cross domain request or not. 6783 */ 6784 var isCrossOrigin = function isCrossOrigin(url) { 6785 var winLoc = window_1.location; 6786 var urlInfo = parseUrl(url); 6787 6788 // IE8 protocol relative urls will return ':' for protocol 6789 var srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; 6790 6791 // Check if url is for another domain/origin 6792 // IE8 doesn't know location.origin, so we won't rely on it here 6793 var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; 6794 6795 return crossOrigin; 6796 }; 6797 6798 var Url = /*#__PURE__*/Object.freeze({ 6799 parseUrl: parseUrl, 6800 getAbsoluteURL: getAbsoluteURL, 6801 getFileExtension: getFileExtension, 6802 isCrossOrigin: isCrossOrigin 6803 }); 6804 6805 var isFunction_1 = isFunction; 6806 6807 var toString$1 = Object.prototype.toString; 6808 6809 function isFunction(fn) { 6810 var string = toString$1.call(fn); 6811 return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && ( 6812 // IE8 and below 6813 fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt); 6814 } 6815 6816 var isFunction$1 = /*#__PURE__*/Object.freeze({ 6817 default: isFunction_1, 6818 __moduleExports: isFunction_1 6819 }); 6820
6821 var trim_1 = createCommonjsModule(function (module, exports) { 6822 exports = module.exports = trim; 6823 6824 function trim(str) { 6825 return str.replace(/^\s*|\s*$/g, ''); 6826 } 6827 6828 exports.left = function (str) { 6829 return str.replace(/^\s*/, ''); 6830 }; 6831 6832 exports.right = function (str) { 6833 return str.replace(/\s*$/, ''); 6834 }; 6835 }); 6836 var trim_2 = trim_1.left; 6837 var trim_3 = trim_1.right; 6838 6839 var trim = /*#__PURE__*/Object.freeze({ 6840 default: trim_1, 6841 __moduleExports: trim_1, 6842 left: trim_2, 6843 right: trim_3 6844 }); 6845 6846 var isFunction$2 = ( isFunction$1 && isFunction_1 ) || isFunction$1; 6847 6848 var forEach_1 = forEach; 6849 6850 var toString$2 = Object.prototype.toString; 6851 var hasOwnProperty = Object.prototype.hasOwnProperty; 6852 6853 function forEach(list, iterator, context) { 6854 if (!isFunction$2(iterator)) { 6855 throw new TypeError('iterator must be a function'); 6856 } 6857 6858 if (arguments.length < 3) { 6859 context = this; 6860 } 6861 6862 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); 6863 } 6864 6865 function forEachArray(array, iterator, context) { 6866 for (var i = 0, len = array.length; i < len; i++) { 6867 if (hasOwnProperty.call(array, i)) { 6868 iterator.call(context, array[i], i, array); 6869 } 6870 } 6871 } 6872 6873 function forEachString(string, iterator, context) { 6874 for (var i = 0, len = string.length; i < len; i++) { 6875 // no such thing as a sparse string. 6876 iterator.call(context, string.charAt(i), i, string); 6877 } 6878 } 6879 6880 function forEachObject(object, iterator, context) { 6881 for (var k in object) { 6882 if (hasOwnProperty.call(object, k)) { 6883 iterator.call(context, object[k], k, object); 6884 } 6885 } 6886 } 6887 6888 var forEach$1 = /*#__PURE__*/Object.freeze({ 6889 default: forEach_1, 6890 __moduleExports: forEach_1 6891 }); 6892 6893 var trim$1 = ( trim && trim_1 ) || trim; 6894 6895 var forEach$2 = ( forEach$1 && forEach_1 ) || forEach$1; 6896 6897 var isArray = function isArray(arg) { 6898 return Object.prototype.toString.call(arg) === '[object Array]'; 6899 }; 6900 6901 var parseHeaders = function parseHeaders(headers) { 6902 if (!headers) return {}; 6903 6904 var result = {}; 6905 6906 forEach$2(trim$1(headers).split('\n'), function (row) { 6907 var index = row.indexOf(':'), 6908 key = trim$1(row.slice(0, index)).toLowerCase(), 6909 value = trim$1(row.slice(index + 1)); 6910 6911 if (typeof result[key] === 'undefined') { 6912 result[key] = value; 6913 } else if (isArray(result[key])) { 6914 result[key].push(value); 6915 } else { 6916 result[key] = [result[key], value]; 6917 } 6918 }); 6919 6920 return result; 6921 }; 6922 6923 var parseHeaders$1 = /*#__PURE__*/Object.freeze({ 6924 default: parseHeaders, 6925 __moduleExports: parseHeaders 6926 }); 6927 6928 var immutable = extend; 6929 6930 var hasOwnProperty$1 = Object.prototype.hasOwnProperty; 6931 6932 function extend() { 6933 var target = {}; 6934 6935 for (var i = 0; i < arguments.length; i++) { 6936 var source = arguments[i]; 6937 6938 for (var key in source) { 6939 if (hasOwnProperty$1.call(source, key)) { 6940 target[key] = source[key]; 6941 } 6942 } 6943 } 6944 6945 return target; 6946 } 6947 6948 var immutable$1 = /*#__PURE__*/Object.freeze({ 6949 default: immutable, 6950 __moduleExports: immutable 6951 }); 6952 6953 var parseHeaders$2 = ( parseHeaders$1 && parseHeaders ) || parseHeaders$1; 6954 6955 var xtend = ( immutable$1 && immutable ) || immutable$1; 6956 6957 var xhr = createXHR; 6958 createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop; 6959 createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest; 6960 6961 forEachArray$1(["get", "put", "post", "patch", "head", "delete"], function (method) { 6962 createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) { 6963 options = initParams(uri, options, callback); 6964 options.method = method.toUpperCase(); 6965 return _createXHR(options); 6966 }; 6967 }); 6968 6969 function forEachArray$1(array, iterator) { 6970 for (var i = 0; i < array.length; i++) { 6971 iterator(array[i]); 6972 } 6973 } 6974 6975 function isEmpty(obj) { 6976 for (var i in obj) { 6977 if (obj.hasOwnProperty(i)) return false;
vendor: 10,824 bytes, lines 6978-7336
6978 } 6979 return true; 6980 } 6981 6982 function initParams(uri, options, callback) { 6983 var params = uri; 6984 6985 if (isFunction$2(options)) { 6986 callback = options; 6987 if (typeof uri === "string") { 6988 params = { uri: uri }; 6989 } 6990 } else { 6991 params = xtend(options, { uri: uri }); 6992 } 6993 6994 params.callback = callback; 6995 return params; 6996 } 6997 6998 function createXHR(uri, options, callback) { 6999 options = initParams(uri, options, callback); 7000 return _createXHR(options); 7001 } 7002 7003 function _createXHR(options) { 7004 if (typeof options.callback === "undefined") { 7005 throw new Error("callback argument missing"); 7006 } 7007 7008 var called = false; 7009 var callback = function cbOnce(err, response, body) { 7010 if (!called) { 7011 called = true; 7012 options.callback(err, response, body); 7013 } 7014 }; 7015 7016 function readystatechange() { 7017 if (xhr.readyState === 4) { 7018 setTimeout(loadFunc, 0); 7019 } 7020 } 7021 7022 function getBody() { 7023 // Chrome with requestType=blob throws errors arround when even testing access to responseText 7024 var body = undefined; 7025 7026 if (xhr.response) { 7027 body = xhr.response; 7028 } else { 7029 body = xhr.responseText || getXml(xhr); 7030 } 7031 7032 if (isJson) { 7033 try { 7034 body = JSON.parse(body); 7035 } catch (e) {} 7036 } 7037 7038 return body; 7039 } 7040 7041 function errorFunc(evt) { 7042 clearTimeout(timeoutTimer); 7043 if (!(evt instanceof Error)) { 7044 evt = new Error("" + (evt || "Unknown XMLHttpRequest Error")); 7045 } 7046 evt.statusCode = 0; 7047 return callback(evt, failureResponse); 7048 } 7049 7050 // will load the data & process the response in a special response object 7051 function loadFunc() { 7052 if (aborted) return; 7053 var status; 7054 clearTimeout(timeoutTimer); 7055 if (options.useXDR && xhr.status === undefined) { 7056 //IE8 CORS GET successful response doesn't have a status field, but body is fine 7057 status = 200; 7058 } else { 7059 status = xhr.status === 1223 ? 204 : xhr.status; 7060 } 7061 var response = failureResponse; 7062 var err = null; 7063 7064 if (status !== 0) { 7065 response = { 7066 body: getBody(), 7067 statusCode: status, 7068 method: method, 7069 headers: {}, 7070 url: uri, 7071 rawRequest: xhr 7072 }; 7073 if (xhr.getAllResponseHeaders) { 7074 //remember xhr can in fact be XDR for CORS in IE 7075 response.headers = parseHeaders$2(xhr.getAllResponseHeaders()); 7076 } 7077 } else { 7078 err = new Error("Internal XMLHttpRequest Error"); 7079 } 7080 return callback(err, response, response.body); 7081 } 7082 7083 var xhr = options.xhr || null; 7084 7085 if (!xhr) { 7086 if (options.cors || options.useXDR) { 7087 xhr = new createXHR.XDomainRequest(); 7088 } else { 7089 xhr = new createXHR.XMLHttpRequest(); 7090 } 7091 } 7092 7093 var key; 7094 var aborted; 7095 var uri = xhr.url = options.uri || options.url; 7096 var method = xhr.method = options.method || "GET"; 7097 var body = options.body || options.data; 7098 var headers = xhr.headers = options.headers || {}; 7099 var sync = !!options.sync; 7100 var isJson = false; 7101 var timeoutTimer; 7102 var failureResponse = { 7103 body: undefined, 7104 headers: {}, 7105 statusCode: 0, 7106 method: method, 7107 url: uri, 7108 rawRequest: xhr 7109 }; 7110 7111 if ("json" in options && options.json !== false) { 7112 isJson = true; 7113 headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user 7114 if (method !== "GET" && method !== "HEAD") { 7115 headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user 7116 body = JSON.stringify(options.json === true ? body : options.json); 7117 } 7118 } 7119 7120 xhr.onreadystatechange = readystatechange; 7121 xhr.onload = loadFunc; 7122 xhr.onerror = errorFunc; 7123 // IE9 must have onprogress be set to a unique function. 7124 xhr.onprogress = function () { 7125 // IE must die 7126 }; 7127 xhr.onabort = function () { 7128 aborted = true; 7129 }; 7130 xhr.ontimeout = errorFunc; 7131 xhr.open(method, uri, !sync, options.username, options.password); 7132 //has to be after open 7133 if (!sync) { 7134 xhr.withCredentials = !!options.withCredentials; 7135 } 7136 // Cannot set timeout with sync request 7137 // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly 7138 // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent 7139 if (!sync && options.timeout > 0) { 7140 timeoutTimer = setTimeout(function () { 7141 if (aborted) return; 7142 aborted = true; //IE9 may still call readystatechange 7143 xhr.abort("timeout"); 7144 var e = new Error("XMLHttpRequest timeout"); 7145 e.code = "ETIMEDOUT"; 7146 errorFunc(e); 7147 }, options.timeout); 7148 } 7149 7150 if (xhr.setRequestHeader) { 7151 for (key in headers) { 7152 if (headers.hasOwnProperty(key)) { 7153 xhr.setRequestHeader(key, headers[key]); 7154 } 7155 } 7156 } else if (options.headers && !isEmpty(options.headers)) { 7157 throw new Error("Headers cannot be set on an XDomainRequest object"); 7158 } 7159 7160 if ("responseType" in options) { 7161 xhr.responseType = options.responseType; 7162 } 7163 7164 if ("beforeSend" in options && typeof options.beforeSend === "function") { 7165 options.beforeSend(xhr); 7166 } 7167 7168 // Microsoft Edge browser sends "undefined" when send is called with undefined value. 7169 // XMLHttpRequest spec says to pass null as body to indicate no body 7170 // See https://github.com/naugtur/xhr/issues/100. 7171 xhr.send(body || null); 7172 7173 return xhr; 7174 } 7175 7176 function getXml(xhr) { 7177 if (xhr.responseType === "document") { 7178 return xhr.responseXML; 7179 } 7180 var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; 7181 if (xhr.responseType === "" && !firefoxBugTakenEffect) { 7182 return xhr.responseXML; 7183 } 7184 7185 return null; 7186 } 7187 7188 function noop() {} 7189 7190 /** 7191 * @file text-track.js 7192 */ 7193 7194 /** 7195 * Takes a webvtt file contents and parses it into cues 7196 * 7197 * @param {string} srcContent 7198 * webVTT file contents 7199 * 7200 * @param {TextTrack} track 7201 * TextTrack to add cues to. Cues come from the srcContent. 7202 * 7203 * @private 7204 */ 7205 var parseCues = function parseCues(srcContent, track) { 7206 var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, window_1.WebVTT.StringDecoder()); 7207 var errors = []; 7208 7209 parser.oncue = function (cue) { 7210 track.addCue(cue); 7211 }; 7212 7213 parser.onparsingerror = function (error) { 7214 errors.push(error); 7215 }; 7216 7217 parser.onflush = function () { 7218 track.trigger({ 7219 type: 'loadeddata', 7220 target: track 7221 }); 7222 }; 7223 7224 parser.parse(srcContent); 7225 if (errors.length > 0) { 7226 if (window_1.console && window_1.console.groupCollapsed) { 7227 window_1.console.groupCollapsed('Text Track parsing errors for ' + track.src); 7228 } 7229 errors.forEach(function (error) { 7230 return log$1.error(error); 7231 }); 7232 if (window_1.console && window_1.console.groupEnd) { 7233 window_1.console.groupEnd(); 7234 } 7235 } 7236 7237 parser.flush(); 7238 }; 7239 7240 /** 7241 * Load a `TextTrack` from a specified url. 7242 * 7243 * @param {string} src 7244 * Url to load track from. 7245 * 7246 * @param {TextTrack} track 7247 * Track to add cues to. Comes from the content at the end of `url`. 7248 * 7249 * @private 7250 */ 7251 var loadTrack = function loadTrack(src, track) { 7252 var opts = { 7253 uri: src 7254 }; 7255 var crossOrigin = isCrossOrigin(src); 7256 7257 if (crossOrigin) { 7258 opts.cors = crossOrigin; 7259 } 7260 7261 xhr(opts, bind(this, function (err, response, responseBody) { 7262 if (err) { 7263 return log$1.error(err, response); 7264 } 7265 7266 track.loaded_ = true; 7267 7268 // Make sure that vttjs has loaded, otherwise, wait till it finished loading 7269 // NOTE: this is only used for the alt/video.novtt.js build 7270 if (typeof window_1.WebVTT !== 'function') { 7271 if (track.tech_) { 7272 var loadHandler = function loadHandler() { 7273 return parseCues(responseBody, track); 7274 }; 7275 7276 track.tech_.on('vttjsloaded', loadHandler); 7277 track.tech_.on('vttjserror', function () { 7278 log$1.error('vttjs failed to load, stopping trying to process ' + track.src); 7279 track.tech_.off('vttjsloaded', loadHandler); 7280 }); 7281 } 7282 } else { 7283 parseCues(responseBody, track); 7284 } 7285 })); 7286 }; 7287 7288 /** 7289 * A representation of a single `TextTrack`. 7290 * 7291 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} 7292 * @extends Track 7293 */ 7294 7295 var TextTrack = function (_Track) { 7296 inherits(TextTrack, _Track); 7297 7298 /** 7299 * Create an instance of this class. 7300 * 7301 * @param {Object} options={} 7302 * Object of option names and values 7303 * 7304 * @param {Tech} options.tech 7305 * A reference to the tech that owns this TextTrack. 7306 * 7307 * @param {TextTrack~Kind} [options.kind='subtitles'] 7308 * A valid text track kind. 7309 * 7310 * @param {TextTrack~Mode} [options.mode='disabled'] 7311 * A valid text track mode. 7312 * 7313 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7314 * A unique id for this TextTrack. 7315 * 7316 * @param {string} [options.label=''] 7317 * The menu label for this track. 7318 * 7319 * @param {string} [options.language=''] 7320 * A valid two character language code. 7321 * 7322 * @param {string} [options.srclang=''] 7323 * A valid two character language code. An alternative, but deprioritized 7324 * version of `options.language` 7325 * 7326 * @param {string} [options.src] 7327 * A url to TextTrack cues. 7328 * 7329 * @param {boolean} [options.default] 7330 * If this track should default to on or off. 7331 */ 7332 function TextTrack() { 7333 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7334 classCallCheck(this, TextTrack); 7335 7336 if (!options.tech) {
vendor: 17,280 bytes, lines 7337-7951
7337 throw new Error('A tech was not provided.'); 7338 } 7339 7340 var settings = mergeOptions(options, { 7341 kind: TextTrackKind[options.kind] || 'subtitles', 7342 language: options.language || options.srclang || '' 7343 }); 7344 var mode = TextTrackMode[settings.mode] || 'disabled'; 7345 var default_ = settings.default; 7346 7347 if (settings.kind === 'metadata' || settings.kind === 'chapters') { 7348 mode = 'hidden'; 7349 } 7350 7351 var _this = possibleConstructorReturn(this, _Track.call(this, settings)); 7352 7353 _this.tech_ = settings.tech; 7354 7355 _this.cues_ = []; 7356 _this.activeCues_ = []; 7357 7358 var cues = new TextTrackCueList(_this.cues_); 7359 var activeCues = new TextTrackCueList(_this.activeCues_); 7360 var changed = false; 7361 var timeupdateHandler = bind(_this, function () { 7362 7363 // Accessing this.activeCues for the side-effects of updating itself 7364 // due to it's nature as a getter function. Do not remove or cues will 7365 // stop updating! 7366 /* eslint-disable no-unused-expressions */ 7367 this.activeCues; 7368 /* eslint-enable no-unused-expressions */ 7369 if (changed) { 7370 this.trigger('cuechange'); 7371 changed = false; 7372 } 7373 }); 7374 7375 if (mode !== 'disabled') { 7376 _this.tech_.ready(function () { 7377 _this.tech_.on('timeupdate', timeupdateHandler); 7378 }, true); 7379 } 7380 7381 Object.defineProperties(_this, { 7382 /** 7383 * @memberof TextTrack 7384 * @member {boolean} default 7385 * If this track was set to be on or off by default. Cannot be changed after 7386 * creation. 7387 * @instance 7388 * 7389 * @readonly 7390 */ 7391 default: { 7392 get: function get$$1() { 7393 return default_; 7394 }, 7395 set: function set$$1() {} 7396 }, 7397 7398 /** 7399 * @memberof TextTrack 7400 * @member {string} mode 7401 * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will 7402 * not be set if setting to an invalid mode. 7403 * @instance 7404 * 7405 * @fires TextTrack#modechange 7406 */ 7407 mode: { 7408 get: function get$$1() { 7409 return mode; 7410 }, 7411 set: function set$$1(newMode) { 7412 var _this2 = this; 7413 7414 if (!TextTrackMode[newMode]) { 7415 return; 7416 } 7417 mode = newMode; 7418 if (mode === 'showing') { 7419 7420 this.tech_.ready(function () { 7421 _this2.tech_.on('timeupdate', timeupdateHandler); 7422 }, true); 7423 } 7424 /** 7425 * An event that fires when mode changes on this track. This allows 7426 * the TextTrackList that holds this track to act accordingly. 7427 * 7428 * > Note: This is not part of the spec! 7429 * 7430 * @event TextTrack#modechange 7431 * @type {EventTarget~Event} 7432 */ 7433 this.trigger('modechange'); 7434 } 7435 }, 7436 7437 /** 7438 * @memberof TextTrack 7439 * @member {TextTrackCueList} cues 7440 * The text track cue list for this TextTrack. 7441 * @instance 7442 */ 7443 cues: { 7444 get: function get$$1() { 7445 if (!this.loaded_) { 7446 return null; 7447 } 7448 7449 return cues; 7450 }, 7451 set: function set$$1() {} 7452 }, 7453 7454 /** 7455 * @memberof TextTrack 7456 * @member {TextTrackCueList} activeCues 7457 * The list text track cues that are currently active for this TextTrack. 7458 * @instance 7459 */ 7460 activeCues: { 7461 get: function get$$1() { 7462 if (!this.loaded_) { 7463 return null; 7464 } 7465 7466 // nothing to do 7467 if (this.cues.length === 0) { 7468 return activeCues; 7469 } 7470 7471 var ct = this.tech_.currentTime(); 7472 var active = []; 7473 7474 for (var i = 0, l = this.cues.length; i < l; i++) { 7475 var cue = this.cues[i]; 7476 7477 if (cue.startTime <= ct && cue.endTime >= ct) { 7478 active.push(cue); 7479 } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { 7480 active.push(cue); 7481 } 7482 } 7483 7484 changed = false; 7485 7486 if (active.length !== this.activeCues_.length) { 7487 changed = true; 7488 } else { 7489 for (var _i = 0; _i < active.length; _i++) { 7490 if (this.activeCues_.indexOf(active[_i]) === -1) { 7491 changed = true; 7492 } 7493 } 7494 } 7495 7496 this.activeCues_ = active; 7497 activeCues.setCues_(this.activeCues_); 7498 7499 return activeCues; 7500 }, 7501 set: function set$$1() {} 7502 } 7503 }); 7504 7505 if (settings.src) { 7506 _this.src = settings.src; 7507 loadTrack(settings.src, _this); 7508 } else { 7509 _this.loaded_ = true; 7510 } 7511 return _this; 7512 } 7513 7514 /** 7515 * Add a cue to the internal list of cues. 7516 * 7517 * @param {TextTrack~Cue} cue 7518 * The cue to add to our internal list 7519 */ 7520 7521 7522 TextTrack.prototype.addCue = function addCue(originalCue) { 7523 var cue = originalCue; 7524 7525 if (window_1.vttjs && !(originalCue instanceof window_1.vttjs.VTTCue)) { 7526 cue = new window_1.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); 7527 7528 for (var prop in originalCue) { 7529 if (!(prop in cue)) { 7530 cue[prop] = originalCue[prop]; 7531 } 7532 } 7533 7534 // make sure that `id` is copied over 7535 cue.id = originalCue.id; 7536 cue.originalCue_ = originalCue; 7537 } 7538 7539 var tracks = this.tech_.textTracks(); 7540 7541 for (var i = 0; i < tracks.length; i++) { 7542 if (tracks[i] !== this) { 7543 tracks[i].removeCue(cue); 7544 } 7545 } 7546 7547 this.cues_.push(cue); 7548 this.cues.setCues_(this.cues_); 7549 }; 7550 7551 /** 7552 * Remove a cue from our internal list 7553 * 7554 * @param {TextTrack~Cue} removeCue 7555 * The cue to remove from our internal list 7556 */ 7557 7558 7559 TextTrack.prototype.removeCue = function removeCue(_removeCue) { 7560 var i = this.cues_.length; 7561 7562 while (i--) { 7563 var cue = this.cues_[i]; 7564 7565 if (cue === _removeCue || cue.originalCue_ && cue.originalCue_ === _removeCue) { 7566 this.cues_.splice(i, 1); 7567 this.cues.setCues_(this.cues_); 7568 break; 7569 } 7570 } 7571 }; 7572 7573 return TextTrack; 7574 }(Track); 7575 7576 /** 7577 * cuechange - One or more cues in the track have become active or stopped being active. 7578 */ 7579 7580 7581 TextTrack.prototype.allowedEvents_ = { 7582 cuechange: 'cuechange' 7583 }; 7584 7585 /** 7586 * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} 7587 * only one `AudioTrack` in the list will be enabled at a time. 7588 * 7589 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} 7590 * @extends Track 7591 */ 7592 7593 var AudioTrack = function (_Track) { 7594 inherits(AudioTrack, _Track); 7595 7596 /** 7597 * Create an instance of this class. 7598 * 7599 * @param {Object} [options={}] 7600 * Object of option names and values 7601 * 7602 * @param {AudioTrack~Kind} [options.kind=''] 7603 * A valid audio track kind 7604 * 7605 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7606 * A unique id for this AudioTrack. 7607 * 7608 * @param {string} [options.label=''] 7609 * The menu label for this track. 7610 * 7611 * @param {string} [options.language=''] 7612 * A valid two character language code. 7613 * 7614 * @param {boolean} [options.enabled] 7615 * If this track is the one that is currently playing. If this track is part of 7616 * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. 7617 */ 7618 function AudioTrack() { 7619 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7620 classCallCheck(this, AudioTrack); 7621 7622 var settings = mergeOptions(options, { 7623 kind: AudioTrackKind[options.kind] || '' 7624 }); 7625 7626 var _this = possibleConstructorReturn(this, _Track.call(this, settings)); 7627 7628 var enabled = false; 7629 7630 /** 7631 * @memberof AudioTrack 7632 * @member {boolean} enabled 7633 * If this `AudioTrack` is enabled or not. When setting this will 7634 * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. 7635 * @instance 7636 * 7637 * @fires VideoTrack#selectedchange 7638 */ 7639 Object.defineProperty(_this, 'enabled', { 7640 get: function get$$1() { 7641 return enabled; 7642 }, 7643 set: function set$$1(newEnabled) { 7644 // an invalid or unchanged value 7645 if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { 7646 return; 7647 } 7648 enabled = newEnabled; 7649 7650 /** 7651 * An event that fires when enabled changes on this track. This allows 7652 * the AudioTrackList that holds this track to act accordingly. 7653 * 7654 * > Note: This is not part of the spec! Native tracks will do 7655 * this internally without an event. 7656 * 7657 * @event AudioTrack#enabledchange 7658 * @type {EventTarget~Event} 7659 */ 7660 this.trigger('enabledchange'); 7661 } 7662 }); 7663 7664 // if the user sets this track to selected then 7665 // set selected to that true value otherwise 7666 // we keep it false 7667 if (settings.enabled) { 7668 _this.enabled = settings.enabled; 7669 } 7670 _this.loaded_ = true; 7671 return _this; 7672 } 7673 7674 return AudioTrack; 7675 }(Track); 7676 7677 /** 7678 * A representation of a single `VideoTrack`. 7679 * 7680 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} 7681 * @extends Track 7682 */ 7683 7684 var VideoTrack = function (_Track) { 7685 inherits(VideoTrack, _Track); 7686 7687 /** 7688 * Create an instance of this class. 7689 * 7690 * @param {Object} [options={}] 7691 * Object of option names and values 7692 * 7693 * @param {string} [options.kind=''] 7694 * A valid {@link VideoTrack~Kind} 7695 * 7696 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7697 * A unique id for this AudioTrack. 7698 * 7699 * @param {string} [options.label=''] 7700 * The menu label for this track. 7701 * 7702 * @param {string} [options.language=''] 7703 * A valid two character language code. 7704 * 7705 * @param {boolean} [options.selected] 7706 * If this track is the one that is currently playing. 7707 */ 7708 function VideoTrack() { 7709 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7710 classCallCheck(this, VideoTrack); 7711 7712 var settings = mergeOptions(options, { 7713 kind: VideoTrackKind[options.kind] || '' 7714 }); 7715 7716 var _this = possibleConstructorReturn(this, _Track.call(this, settings)); 7717 7718 var selected = false; 7719 7720 /** 7721 * @memberof VideoTrack 7722 * @member {boolean} selected 7723 * If this `VideoTrack` is selected or not. When setting this will 7724 * fire {@link VideoTrack#selectedchange} if the state of selected changed. 7725 * @instance 7726 * 7727 * @fires VideoTrack#selectedchange 7728 */ 7729 Object.defineProperty(_this, 'selected', { 7730 get: function get$$1() { 7731 return selected; 7732 }, 7733 set: function set$$1(newSelected) { 7734 // an invalid or unchanged value 7735 if (typeof newSelected !== 'boolean' || newSelected === selected) { 7736 return; 7737 } 7738 selected = newSelected; 7739 7740 /** 7741 * An event that fires when selected changes on this track. This allows 7742 * the VideoTrackList that holds this track to act accordingly. 7743 * 7744 * > Note: This is not part of the spec! Native tracks will do 7745 * this internally without an event. 7746 * 7747 * @event VideoTrack#selectedchange 7748 * @type {EventTarget~Event} 7749 */ 7750 this.trigger('selectedchange'); 7751 } 7752 }); 7753 7754 // if the user sets this track to selected then 7755 // set selected to that true value otherwise 7756 // we keep it false 7757 if (settings.selected) { 7758 _this.selected = settings.selected; 7759 } 7760 return _this; 7761 } 7762 7763 return VideoTrack; 7764 }(Track); 7765 7766 /** 7767 * @file html-track-element.js 7768 */ 7769 7770 /** 7771 * @memberof HTMLTrackElement 7772 * @typedef {HTMLTrackElement~ReadyState} 7773 * @enum {number} 7774 */ 7775 var NONE = 0; 7776 var LOADING = 1; 7777 var LOADED = 2; 7778 var ERROR = 3; 7779 7780 /** 7781 * A single track represented in the DOM. 7782 * 7783 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} 7784 * @extends EventTarget 7785 */ 7786 7787 var HTMLTrackElement = function (_EventTarget) { 7788 inherits(HTMLTrackElement, _EventTarget); 7789 7790 /** 7791 * Create an instance of this class. 7792 * 7793 * @param {Object} options={} 7794 * Object of option names and values 7795 * 7796 * @param {Tech} options.tech 7797 * A reference to the tech that owns this HTMLTrackElement. 7798 * 7799 * @param {TextTrack~Kind} [options.kind='subtitles'] 7800 * A valid text track kind. 7801 * 7802 * @param {TextTrack~Mode} [options.mode='disabled'] 7803 * A valid text track mode. 7804 * 7805 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7806 * A unique id for this TextTrack. 7807 * 7808 * @param {string} [options.label=''] 7809 * The menu label for this track. 7810 * 7811 * @param {string} [options.language=''] 7812 * A valid two character language code. 7813 * 7814 * @param {string} [options.srclang=''] 7815 * A valid two character language code. An alternative, but deprioritized 7816 * vesion of `options.language` 7817 * 7818 * @param {string} [options.src] 7819 * A url to TextTrack cues. 7820 * 7821 * @param {boolean} [options.default] 7822 * If this track should default to on or off. 7823 */ 7824 function HTMLTrackElement() { 7825 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7826 classCallCheck(this, HTMLTrackElement); 7827 7828 var _this = possibleConstructorReturn(this, _EventTarget.call(this)); 7829 7830 var readyState = void 0; 7831 7832 var track = new TextTrack(options); 7833 7834 _this.kind = track.kind; 7835 _this.src = track.src; 7836 _this.srclang = track.language; 7837 _this.label = track.label; 7838 _this.default = track.default; 7839 7840 Object.defineProperties(_this, { 7841 7842 /** 7843 * @memberof HTMLTrackElement 7844 * @member {HTMLTrackElement~ReadyState} readyState 7845 * The current ready state of the track element. 7846 * @instance 7847 */ 7848 readyState: { 7849 get: function get$$1() { 7850 return readyState; 7851 } 7852 }, 7853 7854 /** 7855 * @memberof HTMLTrackElement 7856 * @member {TextTrack} track 7857 * The underlying TextTrack object. 7858 * @instance 7859 * 7860 */ 7861 track: { 7862 get: function get$$1() { 7863 return track; 7864 } 7865 } 7866 }); 7867 7868 readyState = NONE; 7869 7870 /** 7871 * @listens TextTrack#loadeddata 7872 * @fires HTMLTrackElement#load 7873 */ 7874 track.addEventListener('loadeddata', function () { 7875 readyState = LOADED; 7876 7877 _this.trigger({ 7878 type: 'load', 7879 target: _this 7880 }); 7881 }); 7882 return _this; 7883 } 7884 7885 return HTMLTrackElement; 7886 }(EventTarget); 7887 7888 HTMLTrackElement.prototype.allowedEvents_ = { 7889 load: 'load' 7890 }; 7891 7892 HTMLTrackElement.NONE = NONE; 7893 HTMLTrackElement.LOADING = LOADING; 7894 HTMLTrackElement.LOADED = LOADED; 7895 HTMLTrackElement.ERROR = ERROR; 7896 7897 /* 7898 * This file contains all track properties that are used in 7899 * player.js, tech.js, html5.js and possibly other techs in the future. 7900 */ 7901 7902 var NORMAL = { 7903 audio: { 7904 ListClass: AudioTrackList, 7905 TrackClass: AudioTrack, 7906 capitalName: 'Audio' 7907 }, 7908 video: { 7909 ListClass: VideoTrackList, 7910 TrackClass: VideoTrack, 7911 capitalName: 'Video' 7912 }, 7913 text: { 7914 ListClass: TextTrackList, 7915 TrackClass: TextTrack, 7916 capitalName: 'Text' 7917 } 7918 }; 7919 7920 Object.keys(NORMAL).forEach(function (type) { 7921 NORMAL[type].getterName = type + 'Tracks'; 7922 NORMAL[type].privateName = type + 'Tracks_'; 7923 }); 7924 7925 var REMOTE = { 7926 remoteText: { 7927 ListClass: TextTrackList, 7928 TrackClass: TextTrack, 7929 capitalName: 'RemoteText', 7930 getterName: 'remoteTextTracks', 7931 privateName: 'remoteTextTracks_' 7932 }, 7933 remoteTextEl: { 7934 ListClass: HtmlTrackElementList, 7935 TrackClass: HTMLTrackElement, 7936 capitalName: 'RemoteTextTrackEls', 7937 getterName: 'remoteTextTrackEls', 7938 privateName: 'remoteTextTrackEls_' 7939 } 7940 }; 7941 7942 var ALL = mergeOptions(NORMAL, REMOTE); 7943 7944 REMOTE.names = Object.keys(REMOTE); 7945 NORMAL.names = Object.keys(NORMAL); 7946 ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); 7947 7948 /** 7949 * Copyright 2013 vtt.js Contributors 7950 * 7951 * Licensed under the Apache License, Version 2.0 (the "License");
7952 * you may not use this file except in compliance with the License. 7953 * You may obtain a copy of the License at 7954 * 7955 * http://www.apache.org/licenses/LICENSE-2.0 7956 * 7957 * Unless required by applicable law or agreed to in writing, software 7958 * distributed under the License is distributed on an "AS IS" BASIS, 7959 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 7960 * See the License for the specific language governing permissions and 7961 * limitations under the License. 7962 */ 7963 7964 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 7965 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 7966 var _objCreate = Object.create || function () { 7967 function F() {} 7968 return function (o) { 7969 if (arguments.length !== 1) { 7970 throw new Error('Object.create shim only accepts one parameter.'); 7971 } 7972 F.prototype = o; 7973 return new F(); 7974 }; 7975 }(); 7976 7977 // Creates a new ParserError object from an errorData object. The errorData 7978 // object should have default code and message properties. The default message 7979 // property can be overriden by passing in a message parameter. 7980 // See ParsingError.Errors below for acceptable errors. 7981 function ParsingError(errorData, message) { 7982 this.name = "ParsingError"; 7983 this.code = errorData.code; 7984 this.message = message || errorData.message; 7985 } 7986 ParsingError.prototype = _objCreate(Error.prototype); 7987 ParsingError.prototype.constructor = ParsingError; 7988 7989 // ParsingError metadata for acceptable ParsingErrors. 7990 ParsingError.Errors = { 7991 BadSignature: { 7992 code: 0, 7993 message: "Malformed WebVTT signature." 7994 }, 7995 BadTimeStamp: { 7996 code: 1, 7997 message: "Malformed time stamp." 7998 } 7999 }; 8000 8001 // Try to parse input as a time stamp. 8002 function parseTimeStamp(input) { 8003 8004 function computeSeconds(h, m, s, f) { 8005 return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; 8006 } 8007 8008 var m = input.match(/^(\d+):(\d{2})(:\d{2})?\.(\d{3})/); 8009 if (!m) { 8010 return null; 8011 } 8012 8013 if (m[3]) { 8014 // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] 8015 return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); 8016 } else if (m[1] > 59) { 8017 // Timestamp takes the form of [hours]:[minutes].[milliseconds] 8018 // First position is hours as it's over 59. 8019 return computeSeconds(m[1], m[2], 0, m[4]); 8020 } else { 8021 // Timestamp takes the form of [minutes]:[seconds].[milliseconds] 8022 return computeSeconds(0, m[1], m[2], m[4]); 8023 } 8024 } 8025 8026 // A settings object holds key/value pairs and will ignore anything but the first 8027 // assignment to a specific key. 8028 function Settings() { 8029 this.values = _objCreate(null); 8030 } 8031 8032 Settings.prototype = { 8033 // Only accept the first assignment to any key. 8034 set: function set(k, v) { 8035 if (!this.get(k) && v !== "") { 8036 this.values[k] = v; 8037 } 8038 }, 8039 // Return the value for a key, or a default value. 8040 // If 'defaultKey' is passed then 'dflt' is assumed to be an object with 8041 // a number of possible default values as properties where 'defaultKey' is 8042 // the key of the property that will be chosen; otherwise it's assumed to be 8043 // a single value. 8044 get: function get(k, dflt, defaultKey) { 8045 if (defaultKey) { 8046 return this.has(k) ? this.values[k] : dflt[defaultKey]; 8047 } 8048 return this.has(k) ? this.values[k] : dflt; 8049 }, 8050 // Check whether we have a value for a key. 8051 has: function has(k) { 8052 return k in this.values; 8053 }, 8054 // Accept a setting if its one of the given alternatives. 8055 alt: function alt(k, v, a) { 8056 for (var n = 0; n < a.length; ++n) { 8057 if (v === a[n]) { 8058 this.set(k, v); 8059 break; 8060 } 8061 } 8062 }, 8063 // Accept a setting if its a valid (signed) integer. 8064 integer: function integer(k, v) { 8065 if (/^-?\d+$/.test(v)) { 8066 // integer 8067 this.set(k, parseInt(v, 10)); 8068 } 8069 }, 8070 // Accept a setting if its a valid percentage. 8071 percent: function percent(k, v) { 8072 var m; 8073 if (m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) { 8074 v = parseFloat(v); 8075 if (v >= 0 && v <= 100) { 8076 this.set(k, v); 8077 return true; 8078 } 8079 } 8080 return false;
vendor: 9,025 bytes, lines 8081-8347
8081 } 8082 }; 8083 8084 // Helper function to parse input into groups separated by 'groupDelim', and 8085 // interprete each group as a key/value pair separated by 'keyValueDelim'. 8086 function parseOptions(input, callback, keyValueDelim, groupDelim) { 8087 var groups = groupDelim ? input.split(groupDelim) : [input]; 8088 for (var i in groups) { 8089 if (typeof groups[i] !== "string") { 8090 continue; 8091 } 8092 var kv = groups[i].split(keyValueDelim); 8093 if (kv.length !== 2) { 8094 continue; 8095 } 8096 var k = kv[0]; 8097 var v = kv[1]; 8098 callback(k, v); 8099 } 8100 } 8101 8102 function parseCue(input, cue, regionList) { 8103 // Remember the original input if we need to throw an error. 8104 var oInput = input; 8105 // 4.1 WebVTT timestamp 8106 function consumeTimeStamp() { 8107 var ts = parseTimeStamp(input); 8108 if (ts === null) { 8109 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput); 8110 } 8111 // Remove time stamp from input. 8112 input = input.replace(/^[^\sa-zA-Z-]+/, ""); 8113 return ts; 8114 } 8115 8116 // 4.4.2 WebVTT cue settings 8117 function consumeCueSettings(input, cue) { 8118 var settings = new Settings(); 8119 8120 parseOptions(input, function (k, v) { 8121 switch (k) { 8122 case "region": 8123 // Find the last region we parsed with the same region id. 8124 for (var i = regionList.length - 1; i >= 0; i--) { 8125 if (regionList[i].id === v) { 8126 settings.set(k, regionList[i].region); 8127 break; 8128 } 8129 } 8130 break; 8131 case "vertical": 8132 settings.alt(k, v, ["rl", "lr"]); 8133 break; 8134 case "line": 8135 var vals = v.split(","), 8136 vals0 = vals[0]; 8137 settings.integer(k, vals0); 8138 settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; 8139 settings.alt(k, vals0, ["auto"]); 8140 if (vals.length === 2) { 8141 settings.alt("lineAlign", vals[1], ["start", "middle", "end"]); 8142 } 8143 break; 8144 case "position": 8145 vals = v.split(","); 8146 settings.percent(k, vals[0]); 8147 if (vals.length === 2) { 8148 settings.alt("positionAlign", vals[1], ["start", "middle", "end"]); 8149 } 8150 break; 8151 case "size": 8152 settings.percent(k, v); 8153 break; 8154 case "align": 8155 settings.alt(k, v, ["start", "middle", "end", "left", "right"]); 8156 break; 8157 } 8158 }, /:/, /\s/); 8159 8160 // Apply default values for any missing fields. 8161 cue.region = settings.get("region", null); 8162 cue.vertical = settings.get("vertical", ""); 8163 cue.line = settings.get("line", "auto"); 8164 cue.lineAlign = settings.get("lineAlign", "start"); 8165 cue.snapToLines = settings.get("snapToLines", true); 8166 cue.size = settings.get("size", 100); 8167 cue.align = settings.get("align", "middle"); 8168 cue.position = settings.get("position", { 8169 start: 0, 8170 left: 0, 8171 middle: 50, 8172 end: 100, 8173 right: 100 8174 }, cue.align); 8175 cue.positionAlign = settings.get("positionAlign", { 8176 start: "start", 8177 left: "start", 8178 middle: "middle", 8179 end: "end", 8180 right: "end" 8181 }, cue.align); 8182 } 8183 8184 function skipWhitespace() { 8185 input = input.replace(/^\s+/, ""); 8186 } 8187 8188 // 4.1 WebVTT cue timings. 8189 skipWhitespace(); 8190 cue.startTime = consumeTimeStamp(); // (1) collect cue start time 8191 skipWhitespace(); 8192 if (input.substr(0, 3) !== "-->") { 8193 // (3) next characters must match "-->" 8194 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput); 8195 } 8196 input = input.substr(3); 8197 skipWhitespace(); 8198 cue.endTime = consumeTimeStamp(); // (5) collect cue end time 8199 8200 // 4.1 WebVTT cue settings list. 8201 skipWhitespace(); 8202 consumeCueSettings(input, cue); 8203 } 8204 8205 var ESCAPE = { 8206 "&": "&", 8207 "<": "<", 8208 ">": ">", 8209 "‎": "\u200E", 8210 "‏": "\u200F", 8211 " ": "\xA0" 8212 }; 8213 8214 var TAG_NAME = { 8215 c: "span", 8216 i: "i", 8217 b: "b", 8218 u: "u", 8219 ruby: "ruby", 8220 rt: "rt", 8221 v: "span", 8222 lang: "span" 8223 }; 8224 8225 var TAG_ANNOTATION = { 8226 v: "title", 8227 lang: "lang" 8228 }; 8229 8230 var NEEDS_PARENT = { 8231 rt: "ruby" 8232 }; 8233 8234 // Parse content into a document fragment. 8235 function parseContent(window, input) { 8236 function nextToken() { 8237 // Check for end-of-string. 8238 if (!input) { 8239 return null; 8240 } 8241 8242 // Consume 'n' characters from the input. 8243 function consume(result) { 8244 input = input.substr(result.length); 8245 return result; 8246 } 8247 8248 var m = input.match(/^([^<]*)(<[^>]*>?)?/); 8249 // If there is some text before the next tag, return it, otherwise return 8250 // the tag. 8251 return consume(m[1] ? m[1] : m[2]); 8252 } 8253 8254 // Unescape a string 's'. 8255 function unescape1(e) { 8256 return ESCAPE[e]; 8257 } 8258 function unescape(s) { 8259 while (m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/)) { 8260 s = s.replace(m[0], unescape1); 8261 } 8262 return s; 8263 } 8264 8265 function shouldAdd(current, element) { 8266 return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName; 8267 } 8268 8269 // Create an element for this tag. 8270 function createElement(type, annotation) { 8271 var tagName = TAG_NAME[type]; 8272 if (!tagName) { 8273 return null; 8274 } 8275 var element = window.document.createElement(tagName); 8276 element.localName = tagName; 8277 var name = TAG_ANNOTATION[type]; 8278 if (name && annotation) { 8279 element[name] = annotation.trim(); 8280 } 8281 return element; 8282 } 8283 8284 var rootDiv = window.document.createElement("div"), 8285 current = rootDiv, 8286 t, 8287 tagStack = []; 8288 8289 while ((t = nextToken()) !== null) { 8290 if (t[0] === '<') { 8291 if (t[1] === "/") { 8292 // If the closing tag matches, move back up to the parent node. 8293 if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { 8294 tagStack.pop(); 8295 current = current.parentNode; 8296 } 8297 // Otherwise just ignore the end tag. 8298 continue; 8299 } 8300 var ts = parseTimeStamp(t.substr(1, t.length - 2)); 8301 var node; 8302 if (ts) { 8303 // Timestamps are lead nodes as well. 8304 node = window.document.createProcessingInstruction("timestamp", ts); 8305 current.appendChild(node); 8306 continue; 8307 } 8308 var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); 8309 // If we can't parse the tag, skip to the next tag. 8310 if (!m) { 8311 continue; 8312 } 8313 // Try to construct an element, and ignore the tag if we couldn't. 8314 node = createElement(m[1], m[3]); 8315 if (!node) { 8316 continue; 8317 } 8318 // Determine if the tag should be added based on the context of where it 8319 // is placed in the cuetext. 8320 if (!shouldAdd(current, node)) { 8321 continue; 8322 } 8323 // Set the class list (as a list of classes, separated by space). 8324 if (m[2]) { 8325 node.className = m[2].substr(1).replace('.', ' '); 8326 } 8327 // Append the node to the current node, and enter the scope of the new 8328 // node. 8329 tagStack.push(m[1]); 8330 current.appendChild(node); 8331 current = node; 8332 continue; 8333 } 8334 8335 // Text nodes are leaf nodes. 8336 current.appendChild(window.document.createTextNode(unescape(t))); 8337 } 8338 8339 return rootDiv; 8340 } 8341 8342 // This is a list of all the Unicode characters that have a strong 8343 // right-to-left category. What this means is that these characters are 8344 // written right-to-left for sure. It was generated by pulling all the strong 8345 // right-to-left characters out of the Unicode data table. That table can 8346 // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt 8347 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 8347-8862
83470x1093f], [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]]; 8348 8349 function isStrongRTLChar(charCode) { 8350 for (var i = 0; i < strongRTLRanges.length; i++) { 8351 var currentRange = strongRTLRanges[i]; 8352 if (charCode >= currentRange[0] && charCode <= currentRange[1]) { 8353 return true; 8354 } 8355 } 8356 8357 return false; 8358 } 8359 8360 function determineBidi(cueDiv) { 8361 var nodeStack = [], 8362 text = "", 8363 charCode; 8364 8365 if (!cueDiv || !cueDiv.childNodes) { 8366 return "ltr"; 8367 } 8368 8369 function pushNodes(nodeStack, node) { 8370 for (var i = node.childNodes.length - 1; i >= 0; i--) { 8371 nodeStack.push(node.childNodes[i]); 8372 } 8373 } 8374 8375 function nextTextNode(nodeStack) { 8376 if (!nodeStack || !nodeStack.length) { 8377 return null; 8378 } 8379 8380 var node = nodeStack.pop(), 8381 text = node.textContent || node.innerText; 8382 if (text) { 8383 // TODO: This should match all unicode type B characters (paragraph 8384 // separator characters). See issue #115. 8385 var m = text.match(/^.*(\n|\r)/); 8386 if (m) { 8387 nodeStack.length = 0; 8388 return m[0]; 8389 } 8390 return text; 8391 } 8392 if (node.tagName === "ruby") { 8393 return nextTextNode(nodeStack); 8394 } 8395 if (node.childNodes) { 8396 pushNodes(nodeStack, node); 8397 return nextTextNode(nodeStack); 8398 } 8399 } 8400 8401 pushNodes(nodeStack, cueDiv); 8402 while (text = nextTextNode(nodeStack)) { 8403 for (var i = 0; i < text.length; i++) { 8404 charCode = text.charCodeAt(i); 8405 if (isStrongRTLChar(charCode)) { 8406 return "rtl"; 8407 } 8408 } 8409 } 8410 return "ltr"; 8411 } 8412 8413 function computeLinePos(cue) { 8414 if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) { 8415 return cue.line; 8416 } 8417 if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) { 8418 return -1; 8419 } 8420 var track = cue.track, 8421 trackList = track.textTrackList, 8422 count = 0; 8423 for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { 8424 if (trackList[i].mode === "showing") { 8425 count++; 8426 } 8427 } 8428 return ++count * -1; 8429 } 8430 8431 function StyleBox() {} 8432 8433 // Apply styles to a div. If there is no div passed then it defaults to the 8434 // div on 'this'. 8435 StyleBox.prototype.applyStyles = function (styles, div) { 8436 div = div || this.div; 8437 for (var prop in styles) { 8438 if (styles.hasOwnProperty(prop)) { 8439 div.style[prop] = styles[prop]; 8440 } 8441 } 8442 }; 8443 8444 StyleBox.prototype.formatStyle = function (val, unit) { 8445 return val === 0 ? 0 : val + unit; 8446 }; 8447 8448 // Constructs the computed display state of the cue (a div). Places the div 8449 // into the overlay which should be a block level element (usually a div). 8450 function CueStyleBox(window, cue, styleOptions) { 8451 StyleBox.call(this); 8452 this.cue = cue; 8453 8454 // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will 8455 // have inline positioning and will function as the cue background box. 8456 this.cueDiv = parseContent(window, cue.text); 8457 var styles = { 8458 color: "rgba(255, 255, 255, 1)", 8459 backgroundColor: "rgba(0, 0, 0, 0.8)", 8460 position: "relative", 8461 left: 0, 8462 right: 0, 8463 top: 0, 8464 bottom: 0, 8465 display: "inline", 8466 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 8467 unicodeBidi: "plaintext" 8468 }; 8469 8470 this.applyStyles(styles, this.cueDiv); 8471 8472 // Create an absolutely positioned div that will be used to position the cue 8473 // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS 8474 // mirrors of them except "middle" which is "center" in CSS. 8475 this.div = window.document.createElement("div"); 8476 styles = { 8477 direction: determineBidi(this.cueDiv), 8478 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 8479 unicodeBidi: "plaintext", 8480 textAlign: cue.align === "middle" ? "center" : cue.align, 8481 font: styleOptions.font, 8482 whiteSpace: "pre-line", 8483 position: "absolute" 8484 }; 8485 8486 this.applyStyles(styles); 8487 this.div.appendChild(this.cueDiv); 8488 8489 // Calculate the distance from the reference edge of the viewport to the text 8490 // position of the cue box. The reference edge will be resolved later when 8491 // the box orientation styles are applied. 8492 var textPos = 0; 8493 switch (cue.positionAlign) { 8494 case "start": 8495 textPos = cue.position; 8496 break; 8497 case "middle": 8498 textPos = cue.position - cue.size / 2; 8499 break; 8500 case "end": 8501 textPos = cue.position - cue.size; 8502 break; 8503 } 8504 8505 // Horizontal box orientation; textPos is the distance from the left edge of the 8506 // area to the left edge of the box and cue.size is the distance extending to 8507 // the right from there. 8508 if (cue.vertical === "") { 8509 this.applyStyles({ 8510 left: this.formatStyle(textPos, "%"), 8511 width: this.formatStyle(cue.size, "%") 8512 }); 8513 // Vertical box orientation; textPos is the distance from the top edge of the 8514 // area to the top edge of the box and cue.size is the height extending 8515 // downwards from there. 8516 } else { 8517 this.applyStyles({ 8518 top: this.formatStyle(textPos, "%"), 8519 height: this.formatStyle(cue.size, "%") 8520 }); 8521 } 8522 8523 this.move = function (box) { 8524 this.applyStyles({ 8525 top: this.formatStyle(box.top, "px"), 8526 bottom: this.formatStyle(box.bottom, "px"), 8527 left: this.formatStyle(box.left, "px"), 8528 right: this.formatStyle(box.right, "px"), 8529 height: this.formatStyle(box.height, "px"), 8530 width: this.formatStyle(box.width, "px") 8531 }); 8532 }; 8533 } 8534 CueStyleBox.prototype = _objCreate(StyleBox.prototype); 8535 CueStyleBox.prototype.constructor = CueStyleBox; 8536 8537 // Represents the co-ordinates of an Element in a way that we can easily 8538 // compute things with such as if it overlaps or intersects with another Element. 8539 // Can initialize it with either a StyleBox or another BoxPosition. 8540 function BoxPosition(obj) { 8541 // Either a BoxPosition was passed in and we need to copy it, or a StyleBox 8542 // was passed in and we need to copy the results of 'getBoundingClientRect' 8543 // as the object returned is readonly. All co-ordinate values are in reference 8544 // to the viewport origin (top left). 8545 var lh, height, width, top; 8546 if (obj.div) { 8547 height = obj.div.offsetHeight; 8548 width = obj.div.offsetWidth; 8549 top = obj.div.offsetTop; 8550 8551 var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects(); 8552 obj = obj.div.getBoundingClientRect(); 8553 // In certain cases the outter div will be slightly larger then the sum of 8554 // the inner div's lines. This could be due to bold text, etc, on some platforms. 8555 // In this case we should get the average line height and use that. This will 8556 // result in the desired behaviour. 8557 lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0; 8558 } 8559 this.left = obj.left; 8560 this.right = obj.right; 8561 this.top = obj.top || top; 8562 this.height = obj.height || height; 8563 this.bottom = obj.bottom || top + (obj.height || height); 8564 this.width = obj.width || width; 8565 this.lineHeight = lh !== undefined ? lh : obj.lineHeight; 8566 } 8567 8568 // Move the box along a particular axis. Optionally pass in an amount to move 8569 // the box. If no amount is passed then the default is the line height of the 8570 // box. 8571 BoxPosition.prototype.move = function (axis, toMove) { 8572 toMove = toMove !== undefined ? toMove : this.lineHeight; 8573 switch (axis) { 8574 case "+x": 8575 this.left += toMove; 8576 this.right += toMove; 8577 break; 8578 case "-x": 8579 this.left -= toMove; 8580 this.right -= toMove; 8581 break; 8582 case "+y": 8583 this.top += toMove; 8584 this.bottom += toMove; 8585 break; 8586 case "-y": 8587 this.top -= toMove; 8588 this.bottom -= toMove; 8589 break; 8590 } 8591 }; 8592 8593 // Check if this box overlaps another box, b2. 8594 BoxPosition.prototype.overlaps = function (b2) { 8595 return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top; 8596 }; 8597 8598 // Check if this box overlaps any other boxes in boxes. 8599 BoxPosition.prototype.overlapsAny = function (boxes) { 8600 for (var i = 0; i < boxes.length; i++) { 8601 if (this.overlaps(boxes[i])) { 8602 return true; 8603 } 8604 } 8605 return false; 8606 }; 8607 8608 // Check if this box is within another box. 8609 BoxPosition.prototype.within = function (container) { 8610 return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right; 8611 }; 8612 8613 // Check if this box is entirely within the container or it is overlapping 8614 // on the edge opposite of the axis direction passed. For example, if "+x" is 8615 // passed and the box is overlapping on the left edge of the container, then 8616 // return true. 8617 BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) { 8618 switch (axis) { 8619 case "+x": 8620 return this.left < container.left; 8621 case "-x": 8622 return this.right > container.right; 8623 case "+y": 8624 return this.top < container.top; 8625 case "-y": 8626 return this.bottom > container.bottom; 8627 } 8628 }; 8629 8630 // Find the percentage of the area that this box is overlapping with another 8631 // box. 8632 BoxPosition.prototype.intersectPercentage = function (b2) { 8633 var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), 8634 y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), 8635 intersectArea = x * y; 8636 return intersectArea / (this.height * this.width); 8637 }; 8638 8639 // Convert the positions from this box to CSS compatible positions using 8640 // the reference container's positions. This has to be done because this 8641 // box's positions are in reference to the viewport origin, whereas, CSS 8642 // values are in referecne to their respective edges. 8643 BoxPosition.prototype.toCSSCompatValues = function (reference) { 8644 return { 8645 top: this.top - reference.top, 8646 bottom: reference.bottom - this.bottom, 8647 left: this.left - reference.left, 8648 right: reference.right - this.right, 8649 height: this.height, 8650 width: this.width 8651 }; 8652 }; 8653 8654 // Get an object that represents the box's position without anything extra. 8655 // Can pass a StyleBox, HTMLElement, or another BoxPositon. 8656 BoxPosition.getSimpleBoxPosition = function (obj) { 8657 var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; 8658 var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; 8659 var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; 8660 8661 obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj; 8662 var ret = { 8663 left: obj.left, 8664 right: obj.right, 8665 top: obj.top || top, 8666 height: obj.height || height, 8667 bottom: obj.bottom || top + (obj.height || height), 8668 width: obj.width || width 8669 }; 8670 return ret; 8671 }; 8672 8673 // Move a StyleBox to its specified, or next best, position. The containerBox 8674 // is the box that contains the StyleBox, such as a div. boxPositions are 8675 // a list of other boxes that the styleBox can't overlap with. 8676 function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { 8677 8678 // Find the best position for a cue box, b, on the video. The axis parameter 8679 // is a list of axis, the order of which, it will move the box along. For example: 8680 // Passing ["+x", "-x"] will move the box first along the x axis in the positive 8681 // direction. If it doesn't find a good position for it there it will then move 8682 // it along the x axis in the negative direction. 8683 function findBestPosition(b, axis) { 8684 var bestPosition, 8685 specifiedPosition = new BoxPosition(b), 8686 percentage = 1; // Highest possible so the first thing we get is better. 8687 8688 for (var i = 0; i < axis.length; i++) { 8689 while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) { 8690 b.move(axis[i]); 8691 } 8692 // We found a spot where we aren't overlapping anything. This is our 8693 // best position. 8694 if (b.within(containerBox)) { 8695 return b; 8696 } 8697 var p = b.intersectPercentage(containerBox); 8698 // If we're outside the container box less then we were on our last try 8699 // then remember this position as the best position. 8700 if (percentage > p) { 8701 bestPosition = new BoxPosition(b); 8702 percentage = p; 8703 } 8704 // Reset the box position to the specified position. 8705 b = new BoxPosition(specifiedPosition); 8706 } 8707 return bestPosition || specifiedPosition; 8708 } 8709 8710 var boxPosition = new BoxPosition(styleBox), 8711 cue = styleBox.cue, 8712 linePos = computeLinePos(cue), 8713 axis = []; 8714 8715 // If we have a line number to align the cue to. 8716 if (cue.snapToLines) { 8717 var size; 8718 switch (cue.vertical) { 8719 case "": 8720 axis = ["+y", "-y"]; 8721 size = "height"; 8722 break; 8723 case "rl": 8724 axis = ["+x", "-x"]; 8725 size = "width"; 8726 break; 8727 case "lr": 8728 axis = ["-x", "+x"]; 8729 size = "width"; 8730 break; 8731 } 8732 8733 var step = boxPosition.lineHeight, 8734 position = step * Math.round(linePos), 8735 maxPosition = containerBox[size] + step, 8736 initialAxis = axis[0]; 8737 8738 // If the specified intial position is greater then the max position then 8739 // clamp the box to the amount of steps it would take for the box to 8740 // reach the max position. 8741 if (Math.abs(position) > maxPosition) { 8742 position = position < 0 ? -1 : 1; 8743 position *= Math.ceil(maxPosition / step) * step; 8744 } 8745 8746 // If computed line position returns negative then line numbers are 8747 // relative to the bottom of the video instead of the top. Therefore, we 8748 // need to increase our initial position by the length or width of the 8749 // video, depending on the writing direction, and reverse our axis directions. 8750 if (linePos < 0) { 8751 position += cue.vertical === "" ? containerBox.height : containerBox.width; 8752 axis = axis.reverse(); 8753 } 8754 8755 // Move the box to the specified position. This may not be its best 8756 // position. 8757 boxPosition.move(initialAxis, position); 8758 } else { 8759 // If we have a percentage line value for the cue. 8760 var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100; 8761 8762 switch (cue.lineAlign) { 8763 case "middle": 8764 linePos -= calculatedPercentage / 2; 8765 break; 8766 case "end": 8767 linePos -= calculatedPercentage; 8768 break; 8769 } 8770 8771 // Apply initial line position to the cue box. 8772 switch (cue.vertical) { 8773 case "": 8774 styleBox.applyStyles({ 8775 top: styleBox.formatStyle(linePos, "%") 8776 }); 8777 break; 8778 case "rl": 8779 styleBox.applyStyles({ 8780 left: styleBox.formatStyle(linePos, "%") 8781 }); 8782 break; 8783 case "lr": 8784 styleBox.applyStyles({ 8785 right: styleBox.formatStyle(linePos, "%") 8786 }); 8787 break; 8788 } 8789 8790 axis = ["+y", "-x", "+x", "-y"]; 8791 8792 // Get the box position again after we've applied the specified positioning 8793 // to it. 8794 boxPosition = new BoxPosition(styleBox); 8795 } 8796 8797 var bestPosition = findBestPosition(boxPosition, axis); 8798 styleBox.move(bestPosition.toCSSCompatValues(containerBox)); 8799 } 8800 8801 function WebVTT$1() {} 8802 // Nothing 8803 8804 8805 // Helper to allow strings to be decoded instead of the default binary utf8 data. 8806 WebVTT$1.StringDecoder = function () { 8807 return { 8808 decode: function decode(data) { 8809 if (!data) { 8810 return ""; 8811 } 8812 if (typeof data !== "string") { 8813 throw new Error("Error - expected string data."); 8814 } 8815 return decodeURIComponent(encodeURIComponent(data)); 8816 } 8817 }; 8818 }; 8819 8820 WebVTT$1.convertCueToDOMTree = function (window, cuetext) { 8821 if (!window || !cuetext) { 8822 return null; 8823 } 8824 return parseContent(window, cuetext); 8825 }; 8826 8827 var FONT_SIZE_PERCENT = 0.05; 8828 var FONT_STYLE = "sans-serif"; 8829 var CUE_BACKGROUND_PADDING = "1.5%"; 8830 8831 // Runs the processing model over the cues and regions passed to it. 8832 // @param overlay A block level element (usually a div) that the computed cues 8833 // and regions will be placed into. 8834 WebVTT$1.processCues = function (window, cues, overlay) { 8835 if (!window || !cues || !overlay) { 8836 return null; 8837 } 8838 8839 // Remove all previous children. 8840 while (overlay.firstChild) { 8841 overlay.removeChild(overlay.firstChild); 8842 } 8843 8844 var paddedOverlay = window.document.createElement("div"); 8845 paddedOverlay.style.position = "absolute"; 8846 paddedOverlay.style.left = "0"; 8847 paddedOverlay.style.right = "0"; 8848 paddedOverlay.style.top = "0"; 8849 paddedOverlay.style.bottom = "0"; 8850 paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; 8851 overlay.appendChild(paddedOverlay); 8852 8853 // Determine if we need to compute the display states of the cues. This could 8854 // be the case if a cue's state has been changed since the last computation or 8855 // if it has not been computed yet. 8856 function shouldCompute(cues) { 8857 for (var i = 0; i < cues.length; i++) { 8858 if (cues[i].hasBeenReset || !cues[i].displayState) { 8859 return true; 8860 } 8861 } 8862 return false;
vendor: 16,058 bytes, lines 8863-9396
8863 } 8864 8865 // We don't need to recompute the cues' display states. Just reuse them. 8866 if (!shouldCompute(cues)) { 8867 for (var i = 0; i < cues.length; i++) { 8868 paddedOverlay.appendChild(cues[i].displayState); 8869 } 8870 return; 8871 } 8872 8873 var boxPositions = [], 8874 containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), 8875 fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; 8876 var styleOptions = { 8877 font: fontSize + "px " + FONT_STYLE 8878 }; 8879 8880 (function () { 8881 var styleBox, cue; 8882 8883 for (var i = 0; i < cues.length; i++) { 8884 cue = cues[i]; 8885 8886 // Compute the intial position and styles of the cue div. 8887 styleBox = new CueStyleBox(window, cue, styleOptions); 8888 paddedOverlay.appendChild(styleBox.div); 8889 8890 // Move the cue div to it's correct line position. 8891 moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); 8892 8893 // Remember the computed div so that we don't have to recompute it later 8894 // if we don't have too. 8895 cue.displayState = styleBox.div; 8896 8897 boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); 8898 } 8899 })(); 8900 }; 8901 8902 WebVTT$1.Parser = function (window, vttjs, decoder) { 8903 if (!decoder) { 8904 decoder = vttjs; 8905 vttjs = {}; 8906 } 8907 if (!vttjs) { 8908 vttjs = {}; 8909 } 8910 8911 this.window = window; 8912 this.vttjs = vttjs; 8913 this.state = "INITIAL"; 8914 this.buffer = ""; 8915 this.decoder = decoder || new TextDecoder("utf8"); 8916 this.regionList = []; 8917 }; 8918 8919 WebVTT$1.Parser.prototype = { 8920 // If the error is a ParsingError then report it to the consumer if 8921 // possible. If it's not a ParsingError then throw it like normal. 8922 reportOrThrowError: function reportOrThrowError(e) { 8923 if (e instanceof ParsingError) { 8924 this.onparsingerror && this.onparsingerror(e); 8925 } else { 8926 throw e; 8927 } 8928 }, 8929 parse: function parse(data) { 8930 var self = this; 8931 8932 // If there is no data then we won't decode it, but will just try to parse 8933 // whatever is in buffer already. This may occur in circumstances, for 8934 // example when flush() is called. 8935 if (data) { 8936 // Try to decode the data that we received. 8937 self.buffer += self.decoder.decode(data, { stream: true }); 8938 } 8939 8940 function collectNextLine() { 8941 var buffer = self.buffer; 8942 var pos = 0; 8943 while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { 8944 ++pos; 8945 } 8946 var line = buffer.substr(0, pos); 8947 // Advance the buffer early in case we fail below. 8948 if (buffer[pos] === '\r') { 8949 ++pos; 8950 } 8951 if (buffer[pos] === '\n') { 8952 ++pos; 8953 } 8954 self.buffer = buffer.substr(pos); 8955 return line; 8956 } 8957 8958 // 3.4 WebVTT region and WebVTT region settings syntax 8959 function parseRegion(input) { 8960 var settings = new Settings(); 8961 8962 parseOptions(input, function (k, v) { 8963 switch (k) { 8964 case "id": 8965 settings.set(k, v); 8966 break; 8967 case "width": 8968 settings.percent(k, v); 8969 break; 8970 case "lines": 8971 settings.integer(k, v); 8972 break; 8973 case "regionanchor": 8974 case "viewportanchor": 8975 var xy = v.split(','); 8976 if (xy.length !== 2) { 8977 break; 8978 } 8979 // We have to make sure both x and y parse, so use a temporary 8980 // settings object here. 8981 var anchor = new Settings(); 8982 anchor.percent("x", xy[0]); 8983 anchor.percent("y", xy[1]); 8984 if (!anchor.has("x") || !anchor.has("y")) { 8985 break; 8986 } 8987 settings.set(k + "X", anchor.get("x")); 8988 settings.set(k + "Y", anchor.get("y")); 8989 break; 8990 case "scroll": 8991 settings.alt(k, v, ["up"]); 8992 break; 8993 } 8994 }, /=/, /\s/); 8995 8996 // Create the region, using default values for any values that were not 8997 // specified. 8998 if (settings.has("id")) { 8999 var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); 9000 region.width = settings.get("width", 100); 9001 region.lines = settings.get("lines", 3); 9002 region.regionAnchorX = settings.get("regionanchorX", 0); 9003 region.regionAnchorY = settings.get("regionanchorY", 100); 9004 region.viewportAnchorX = settings.get("viewportanchorX", 0); 9005 region.viewportAnchorY = settings.get("viewportanchorY", 100); 9006 region.scroll = settings.get("scroll", ""); 9007 // Register the region. 9008 self.onregion && self.onregion(region); 9009 // Remember the VTTRegion for later in case we parse any VTTCues that 9010 // reference it. 9011 self.regionList.push({ 9012 id: settings.get("id"), 9013 region: region 9014 }); 9015 } 9016 } 9017 9018 // draft-pantos-http-live-streaming-20 9019 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5 9020 // 3.5 WebVTT 9021 function parseTimestampMap(input) { 9022 var settings = new Settings(); 9023 9024 parseOptions(input, function (k, v) { 9025 switch (k) { 9026 case "MPEGT": 9027 settings.integer(k + 'S', v); 9028 break; 9029 case "LOCA": 9030 settings.set(k + 'L', parseTimeStamp(v)); 9031 break; 9032 } 9033 }, /[^\d]:/, /,/); 9034 9035 self.ontimestampmap && self.ontimestampmap({ 9036 "MPEGTS": settings.get("MPEGTS"), 9037 "LOCAL": settings.get("LOCAL") 9038 }); 9039 } 9040 9041 // 3.2 WebVTT metadata header syntax 9042 function parseHeader(input) { 9043 if (input.match(/X-TIMESTAMP-MAP/)) { 9044 // This line contains HLS X-TIMESTAMP-MAP metadata 9045 parseOptions(input, function (k, v) { 9046 switch (k) { 9047 case "X-TIMESTAMP-MAP": 9048 parseTimestampMap(v); 9049 break; 9050 } 9051 }, /=/); 9052 } else { 9053 parseOptions(input, function (k, v) { 9054 switch (k) { 9055 case "Region": 9056 // 3.3 WebVTT region metadata header syntax 9057 parseRegion(v); 9058 break; 9059 } 9060 }, /:/); 9061 } 9062 } 9063 9064 // 5.1 WebVTT file parsing. 9065 try { 9066 var line; 9067 if (self.state === "INITIAL") { 9068 // We can't start parsing until we have the first line. 9069 if (!/\r\n|\n/.test(self.buffer)) { 9070 return this; 9071 } 9072 9073 line = collectNextLine(); 9074 9075 var m = line.match(/^WEBVTT([ \t].*)?$/); 9076 if (!m || !m[0]) { 9077 throw new ParsingError(ParsingError.Errors.BadSignature); 9078 } 9079 9080 self.state = "HEADER"; 9081 } 9082 9083 var alreadyCollectedLine = false; 9084 while (self.buffer) { 9085 // We can't parse a line until we have the full line. 9086 if (!/\r\n|\n/.test(self.buffer)) { 9087 return this; 9088 } 9089 9090 if (!alreadyCollectedLine) { 9091 line = collectNextLine(); 9092 } else { 9093 alreadyCollectedLine = false; 9094 } 9095 9096 switch (self.state) { 9097 case "HEADER": 9098 // 13-18 - Allow a header (metadata) under the WEBVTT line. 9099 if (/:/.test(line)) { 9100 parseHeader(line); 9101 } else if (!line) { 9102 // An empty line terminates the header and starts the body (cues). 9103 self.state = "ID"; 9104 } 9105 continue; 9106 case "NOTE": 9107 // Ignore NOTE blocks. 9108 if (!line) { 9109 self.state = "ID"; 9110 } 9111 continue; 9112 case "ID": 9113 // Check for the start of NOTE blocks. 9114 if (/^NOTE($|[ \t])/.test(line)) { 9115 self.state = "NOTE"; 9116 break; 9117 } 9118 // 19-29 - Allow any number of line terminators, then initialize new cue values. 9119 if (!line) { 9120 continue; 9121 } 9122 self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); 9123 self.state = "CUE"; 9124 // 30-39 - Check if self line contains an optional identifier or timing data. 9125 if (line.indexOf("-->") === -1) { 9126 self.cue.id = line; 9127 continue; 9128 } 9129 // Process line as start of a cue. 9130 /*falls through*/ 9131 case "CUE": 9132 // 40 - Collect cue timings and settings. 9133 try { 9134 parseCue(line, self.cue, self.regionList); 9135 } catch (e) { 9136 self.reportOrThrowError(e); 9137 // In case of an error ignore rest of the cue. 9138 self.cue = null; 9139 self.state = "BADCUE"; 9140 continue; 9141 } 9142 self.state = "CUETEXT"; 9143 continue; 9144 case "CUETEXT": 9145 var hasSubstring = line.indexOf("-->") !== -1; 9146 // 34 - If we have an empty line then report the cue. 9147 // 35 - If we have the special substring '-->' then report the cue, 9148 // but do not collect the line as we need to process the current 9149 // one as a new cue. 9150 if (!line || hasSubstring && (alreadyCollectedLine = true)) { 9151 // We are done parsing self cue. 9152 self.oncue && self.oncue(self.cue); 9153 self.cue = null; 9154 self.state = "ID"; 9155 continue; 9156 } 9157 if (self.cue.text) { 9158 self.cue.text += "\n"; 9159 } 9160 self.cue.text += line; 9161 continue; 9162 case "BADCUE": 9163 // BADCUE 9164 // 54-62 - Collect and discard the remaining cue. 9165 if (!line) { 9166 self.state = "ID"; 9167 } 9168 continue; 9169 } 9170 } 9171 } catch (e) { 9172 self.reportOrThrowError(e); 9173 9174 // If we are currently parsing a cue, report what we have. 9175 if (self.state === "CUETEXT" && self.cue && self.oncue) { 9176 self.oncue(self.cue); 9177 } 9178 self.cue = null; 9179 // Enter BADWEBVTT state if header was not parsed correctly otherwise 9180 // another exception occurred so enter BADCUE state. 9181 self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; 9182 } 9183 return this; 9184 }, 9185 flush: function flush() { 9186 var self = this; 9187 try { 9188 // Finish decoding the stream. 9189 self.buffer += self.decoder.decode(); 9190 // Synthesize the end of the current cue or region. 9191 if (self.cue || self.state === "HEADER") { 9192 self.buffer += "\n\n"; 9193 self.parse(); 9194 } 9195 // If we've flushed, parsed, and we're still on the INITIAL state then 9196 // that means we don't have enough of the stream to parse the first 9197 // line. 9198 if (self.state === "INITIAL") { 9199 throw new ParsingError(ParsingError.Errors.BadSignature); 9200 } 9201 } catch (e) { 9202 self.reportOrThrowError(e); 9203 } 9204 self.onflush && self.onflush(); 9205 return this; 9206 } 9207 }; 9208 9209 var vtt = WebVTT$1; 9210 9211 var vtt$1 = /*#__PURE__*/Object.freeze({ 9212 default: vtt, 9213 __moduleExports: vtt 9214 }); 9215 9216 /** 9217 * Copyright 2013 vtt.js Contributors 9218 * 9219 * Licensed under the Apache License, Version 2.0 (the "License"); 9220 * you may not use this file except in compliance with the License. 9221 * You may obtain a copy of the License at 9222 * 9223 * http://www.apache.org/licenses/LICENSE-2.0 9224 * 9225 * Unless required by applicable law or agreed to in writing, software 9226 * distributed under the License is distributed on an "AS IS" BASIS, 9227 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9228 * See the License for the specific language governing permissions and 9229 * limitations under the License. 9230 */ 9231 9232 var autoKeyword = "auto"; 9233 var directionSetting = { 9234 "": 1, 9235 "lr": 1, 9236 "rl": 1 9237 }; 9238 var alignSetting = { 9239 "start": 1, 9240 "middle": 1, 9241 "end": 1, 9242 "left": 1, 9243 "right": 1 9244 }; 9245 9246 function findDirectionSetting(value) { 9247 if (typeof value !== "string") { 9248 return false; 9249 } 9250 var dir = directionSetting[value.toLowerCase()]; 9251 return dir ? value.toLowerCase() : false; 9252 } 9253 9254 function findAlignSetting(value) { 9255 if (typeof value !== "string") { 9256 return false; 9257 } 9258 var align = alignSetting[value.toLowerCase()]; 9259 return align ? value.toLowerCase() : false; 9260 } 9261 9262 function VTTCue(startTime, endTime, text) { 9263 /** 9264 * Shim implementation specific properties. These properties are not in 9265 * the spec. 9266 */ 9267 9268 // Lets us know when the VTTCue's data has changed in such a way that we need 9269 // to recompute its display state. This lets us compute its display state 9270 // lazily. 9271 this.hasBeenReset = false; 9272 9273 /** 9274 * VTTCue and TextTrackCue properties 9275 * http://dev.w3.org/html5/webvtt/#vttcue-interface 9276 */ 9277 9278 var _id = ""; 9279 var _pauseOnExit = false; 9280 var _startTime = startTime; 9281 var _endTime = endTime; 9282 var _text = text; 9283 var _region = null; 9284 var _vertical = ""; 9285 var _snapToLines = true; 9286 var _line = "auto"; 9287 var _lineAlign = "start"; 9288 var _position = 50; 9289 var _positionAlign = "middle"; 9290 var _size = 50; 9291 var _align = "middle"; 9292 9293 Object.defineProperties(this, { 9294 "id": { 9295 enumerable: true, 9296 get: function get() { 9297 return _id; 9298 }, 9299 set: function set(value) { 9300 _id = "" + value; 9301 } 9302 }, 9303 9304 "pauseOnExit": { 9305 enumerable: true, 9306 get: function get() { 9307 return _pauseOnExit; 9308 }, 9309 set: function set(value) { 9310 _pauseOnExit = !!value; 9311 } 9312 }, 9313 9314 "startTime": { 9315 enumerable: true, 9316 get: function get() { 9317 return _startTime; 9318 }, 9319 set: function set(value) { 9320 if (typeof value !== "number") { 9321 throw new TypeError("Start time must be set to a number."); 9322 } 9323 _startTime = value; 9324 this.hasBeenReset = true; 9325 } 9326 }, 9327 9328 "endTime": { 9329 enumerable: true, 9330 get: function get() { 9331 return _endTime; 9332 }, 9333 set: function set(value) { 9334 if (typeof value !== "number") { 9335 throw new TypeError("End time must be set to a number."); 9336 } 9337 _endTime = value; 9338 this.hasBeenReset = true; 9339 } 9340 }, 9341 9342 "text": { 9343 enumerable: true, 9344 get: function get() { 9345 return _text; 9346 }, 9347 set: function set(value) { 9348 _text = "" + value; 9349 this.hasBeenReset = true; 9350 } 9351 }, 9352 9353 "region": { 9354 enumerable: true, 9355 get: function get() { 9356 return _region; 9357 }, 9358 set: function set(value) { 9359 _region = value; 9360 this.hasBeenReset = true; 9361 } 9362 }, 9363 9364 "vertical": { 9365 enumerable: true, 9366 get: function get() { 9367 return _vertical; 9368 }, 9369 set: function set(value) { 9370 var setting = findDirectionSetting(value); 9371 // Have to check for false because the setting an be an empty string. 9372 if (setting === false) { 9373 throw new SyntaxError("An invalid or illegal string was specified."); 9374 } 9375 _vertical = setting; 9376 this.hasBeenReset = true; 9377 } 9378 }, 9379 9380 "snapToLines": { 9381 enumerable: true, 9382 get: function get() { 9383 return _snapToLines; 9384 }, 9385 set: function set(value) { 9386 _snapToLines = !!value; 9387 this.hasBeenReset = true; 9388 } 9389 }, 9390 9391 "line": { 9392 enumerable: true, 9393 get: function get() { 9394 return _line; 9395 }, 9396 set: function set(value) {
vendor: 4,519 bytes, lines 9397-9564
9397 if (typeof value !== "number" && value !== autoKeyword) { 9398 throw new SyntaxError("An invalid number or illegal string was specified."); 9399 } 9400 _line = value; 9401 this.hasBeenReset = true; 9402 } 9403 }, 9404 9405 "lineAlign": { 9406 enumerable: true, 9407 get: function get() { 9408 return _lineAlign; 9409 }, 9410 set: function set(value) { 9411 var setting = findAlignSetting(value); 9412 if (!setting) { 9413 throw new SyntaxError("An invalid or illegal string was specified."); 9414 } 9415 _lineAlign = setting; 9416 this.hasBeenReset = true; 9417 } 9418 }, 9419 9420 "position": { 9421 enumerable: true, 9422 get: function get() { 9423 return _position; 9424 }, 9425 set: function set(value) { 9426 if (value < 0 || value > 100) { 9427 throw new Error("Position must be between 0 and 100."); 9428 } 9429 _position = value; 9430 this.hasBeenReset = true; 9431 } 9432 }, 9433 9434 "positionAlign": { 9435 enumerable: true, 9436 get: function get() { 9437 return _positionAlign; 9438 }, 9439 set: function set(value) { 9440 var setting = findAlignSetting(value); 9441 if (!setting) { 9442 throw new SyntaxError("An invalid or illegal string was specified."); 9443 } 9444 _positionAlign = setting; 9445 this.hasBeenReset = true; 9446 } 9447 }, 9448 9449 "size": { 9450 enumerable: true, 9451 get: function get() { 9452 return _size; 9453 }, 9454 set: function set(value) { 9455 if (value < 0 || value > 100) { 9456 throw new Error("Size must be between 0 and 100."); 9457 } 9458 _size = value; 9459 this.hasBeenReset = true; 9460 } 9461 }, 9462 9463 "align": { 9464 enumerable: true, 9465 get: function get() { 9466 return _align; 9467 }, 9468 set: function set(value) { 9469 var setting = findAlignSetting(value); 9470 if (!setting) { 9471 throw new SyntaxError("An invalid or illegal string was specified."); 9472 } 9473 _align = setting; 9474 this.hasBeenReset = true; 9475 } 9476 } 9477 }); 9478 9479 /** 9480 * Other <track> spec defined properties 9481 */ 9482 9483 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state 9484 this.displayState = undefined; 9485 } 9486 9487 /** 9488 * VTTCue methods 9489 */ 9490 9491 VTTCue.prototype.getCueAsHTML = function () { 9492 // Assume WebVTT.convertCueToDOMTree is on the global. 9493 return WebVTT.convertCueToDOMTree(window, this.text); 9494 }; 9495 9496 var vttcue = VTTCue; 9497 9498 var vttcue$1 = /*#__PURE__*/Object.freeze({ 9499 default: vttcue, 9500 __moduleExports: vttcue 9501 }); 9502 9503 /** 9504 * Copyright 2013 vtt.js Contributors 9505 * 9506 * Licensed under the Apache License, Version 2.0 (the "License"); 9507 * you may not use this file except in compliance with the License. 9508 * You may obtain a copy of the License at 9509 * 9510 * http://www.apache.org/licenses/LICENSE-2.0 9511 * 9512 * Unless required by applicable law or agreed to in writing, software 9513 * distributed under the License is distributed on an "AS IS" BASIS, 9514 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9515 * See the License for the specific language governing permissions and 9516 * limitations under the License. 9517 */ 9518 9519 var scrollSetting = { 9520 "": true, 9521 "up": true 9522 }; 9523 9524 function findScrollSetting(value) { 9525 if (typeof value !== "string") { 9526 return false; 9527 } 9528 var scroll = scrollSetting[value.toLowerCase()]; 9529 return scroll ? value.toLowerCase() : false; 9530 } 9531 9532 function isValidPercentValue(value) { 9533 return typeof value === "number" && value >= 0 && value <= 100; 9534 } 9535 9536 // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface 9537 function VTTRegion() { 9538 var _width = 100; 9539 var _lines = 3; 9540 var _regionAnchorX = 0; 9541 var _regionAnchorY = 100; 9542 var _viewportAnchorX = 0; 9543 var _viewportAnchorY = 100; 9544 var _scroll = ""; 9545 9546 Object.defineProperties(this, { 9547 "width": { 9548 enumerable: true, 9549 get: function get() { 9550 return _width; 9551 }, 9552 set: function set(value) { 9553 if (!isValidPercentValue(value)) { 9554 throw new Error("Width must be between 0 and 100."); 9555 } 9556 _width = value; 9557 } 9558 }, 9559 "lines": { 9560 enumerable: true, 9561 get: function get() { 9562 return _lines; 9563 }, 9564 set: function set(value) {
9565 if (typeof value !== "number") { 9566 throw new TypeError("Lines must be set to a number."); 9567 } 9568 _lines = value; 9569 } 9570 }, 9571 "regionAnchorY": { 9572 enumerable: true, 9573 get: function get() { 9574 return _regionAnchorY; 9575 }, 9576 set: function set(value) { 9577 if (!isValidPercentValue(value)) { 9578 throw new Error("RegionAnchorX must be between 0 and 100."); 9579 } 9580 _regionAnchorY = value; 9581 } 9582 }, 9583 "regionAnchorX": { 9584 enumerable: true, 9585 get: function get() { 9586 return _regionAnchorX; 9587 }, 9588 set: function set(value) { 9589 if (!isValidPercentValue(value)) { 9590 throw new Error("RegionAnchorY must be between 0 and 100."); 9591 } 9592 _regionAnchorX = value; 9593 } 9594 }, 9595 "viewportAnchorY": { 9596 enumerable: true, 9597 get: function get() { 9598 return _viewportAnchorY; 9599 }, 9600 set: function set(value) { 9601 if (!isValidPercentValue(value)) { 9602 throw new Error("ViewportAnchorY must be between 0 and 100."); 9603 } 9604 _viewportAnchorY = value; 9605 } 9606 }, 9607 "viewportAnchorX": { 9608 enumerable: true, 9609 get: function get() { 9610 return _viewportAnchorX; 9611 }, 9612 set: function set(value) { 9613 if (!isValidPercentValue(value)) { 9614 throw new Error("ViewportAnchorX must be between 0 and 100."); 9615 } 9616 _viewportAnchorX = value; 9617 } 9618 }, 9619 "scroll": { 9620 enumerable: true, 9621 get: function get() { 9622 return _scroll; 9623 }, 9624 set: function set(value) { 9625 var setting = findScrollSetting(value); 9626 // Have to check for false as an empty string is a legal value. 9627 if (setting === false) { 9628 throw new SyntaxError("An invalid or illegal string was specified."); 9629 } 9630 _scroll = setting; 9631 } 9632 } 9633 }); 9634 } 9635 9636 var vttregion = VTTRegion; 9637 9638 var vttregion$1 = /*#__PURE__*/Object.freeze({ 9639 default: vttregion, 9640 __moduleExports: vttregion 9641 }); 9642 9643 var require$$0 = ( vtt$1 && vtt ) || vtt$1; 9644 9645 var require$$1 = ( vttcue$1 && vttcue ) || vttcue$1; 9646 9647 var require$$2 = ( vttregion$1 && vttregion ) || vttregion$1; 9648 9649 var browserIndex = createCommonjsModule(function (module) { 9650 /** 9651 * Copyright 2013 vtt.js Contributors 9652 * 9653 * Licensed under the Apache License, Version 2.0 (the "License"); 9654 * you may not use this file except in compliance with the License. 9655 * You may obtain a copy of the License at 9656 * 9657 * http://www.apache.org/licenses/LICENSE-2.0 9658 * 9659 * Unless required by applicable law or agreed to in writing, software 9660 * distributed under the License is distributed on an "AS IS" BASIS, 9661 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9662 * See the License for the specific language governing permissions and 9663 * limitations under the License. 9664 */ 9665 9666 // Default exports for Node. Export the extended versions of VTTCue and 9667 // VTTRegion in Node since we likely want the capability to convert back and 9668 // forth between JSON. If we don't then it's not that big of a deal since we're 9669 // off browser. 9670 9671 9672 var vttjs = module.exports = { 9673 WebVTT: require$$0, 9674 VTTCue: require$$1, 9675 VTTRegion: require$$2 9676 }; 9677 9678 window_1.vttjs = vttjs; 9679 window_1.WebVTT = vttjs.WebVTT; 9680 9681 var cueShim = vttjs.VTTCue; 9682 var regionShim = vttjs.VTTRegion; 9683 var nativeVTTCue = window_1.VTTCue; 9684 var nativeVTTRegion = window_1.VTTRegion; 9685 9686 vttjs.shim = function () { 9687 window_1.VTTCue = cueShim; 9688 window_1.VTTRegion = regionShim; 9689 }; 9690 9691 vttjs.restore = function () { 9692 window_1.VTTCue = nativeVTTCue; 9693 window_1.VTTRegion = nativeVTTRegion; 9694 }; 9695 9696 if (!window_1.VTTCue) { 9697 vttjs.shim(); 9698 } 9699 }); 9700 var browserIndex_1 = browserIndex.WebVTT; 9701 var browserIndex_2 = browserIndex.VTTCue; 9702 var browserIndex_3 = browserIndex.VTTRegion; 9703 9704 /** 9705 * @file tech.js 9706 */ 9707 9708 /** 9709 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string 9710 * that just contains the src url alone. 9711 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` 9712 * `var SourceString = 'http://example.com/some-video.mp4';` 9713 * 9714 * @typedef {Object|string} Tech~SourceObject 9715 * 9716 * @property {string} src 9717 * The url to the source 9718 * 9719 * @property {string} type 9720 * The mime type of the source 9721 */ 9722 9723 /** 9724 * A function used by {@link Tech} to create a new {@link TextTrack}. 9725 * 9726 * @private 9727 * 9728 * @param {Tech} self 9729 * An instance of the Tech class. 9730 * 9731 * @param {string} kind 9732 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 9733 * 9734 * @param {string} [label] 9735 * Label to identify the text track 9736 * 9737 * @param {string} [language] 9738 * Two letter language abbreviation 9739 * 9740 * @param {Object} [options={}] 9741 * An object with additional text track options 9742 * 9743 * @return {TextTrack} 9744 * The text track that was created. 9745 */ 9746 function createTrackHelper(self, kind, label, language) { 9747 var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; 9748 9749 var tracks = self.textTracks(); 9750 9751 options.kind = kind; 9752 9753 if (label) { 9754 options.label = label; 9755 } 9756 if (language) { 9757 options.language = language; 9758 } 9759 options.tech = self; 9760 9761 var track = new ALL.text.TrackClass(options); 9762 9763 tracks.addTrack(track); 9764 9765 return track; 9766 } 9767 9768 /** 9769 * This is the base class for media playback technology controllers, such as 9770 * {@link Flash} and {@link HTML5} 9771 * 9772 * @extends Component 9773 */ 9774 9775 var Tech = function (_Component) { 9776 inherits(Tech, _Component); 9777 9778 /** 9779 * Create an instance of this Tech. 9780 * 9781 * @param {Object} [options] 9782 * The key/value store of player options. 9783 * 9784 * @param {Component~ReadyCallback} ready 9785 * Callback function to call when the `HTML5` Tech is ready. 9786 */ 9787 function Tech() { 9788 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 9789 var ready = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; 9790 classCallCheck(this, Tech); 9791 9792 // we don't want the tech to report user activity automatically.
vendor: 319,771 bytes, lines 9793-20909
9793 // This is done manually in addControlsListeners 9794 options.reportTouchActivity = false; 9795 9796 // keep track of whether the current source has played at all to 9797 // implement a very limited played() 9798 var _this = possibleConstructorReturn(this, _Component.call(this, null, options, ready)); 9799 9800 _this.hasStarted_ = false; 9801 _this.on('playing', function () { 9802 this.hasStarted_ = true; 9803 }); 9804 _this.on('loadstart', function () { 9805 this.hasStarted_ = false; 9806 }); 9807 9808 ALL.names.forEach(function (name) { 9809 var props = ALL[name]; 9810 9811 if (options && options[props.getterName]) { 9812 _this[props.privateName] = options[props.getterName]; 9813 } 9814 }); 9815 9816 // Manually track progress in cases where the browser/flash player doesn't report it. 9817 if (!_this.featuresProgressEvents) { 9818 _this.manualProgressOn(); 9819 } 9820 9821 // Manually track timeupdates in cases where the browser/flash player doesn't report it. 9822 if (!_this.featuresTimeupdateEvents) { 9823 _this.manualTimeUpdatesOn(); 9824 } 9825 9826 ['Text', 'Audio', 'Video'].forEach(function (track) { 9827 if (options['native' + track + 'Tracks'] === false) { 9828 _this['featuresNative' + track + 'Tracks'] = false; 9829 } 9830 }); 9831 9832 if (options.nativeCaptions === false || options.nativeTextTracks === false) { 9833 _this.featuresNativeTextTracks = false; 9834 } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { 9835 _this.featuresNativeTextTracks = true; 9836 } 9837 9838 if (!_this.featuresNativeTextTracks) { 9839 _this.emulateTextTracks(); 9840 } 9841 9842 _this.autoRemoteTextTracks_ = new ALL.text.ListClass(); 9843 9844 _this.initTrackListeners(); 9845 9846 // Turn on component tap events only if not using native controls 9847 if (!options.nativeControlsForTouch) { 9848 _this.emitTapEvents(); 9849 } 9850 9851 if (_this.constructor) { 9852 _this.name_ = _this.constructor.name || 'Unknown Tech'; 9853 } 9854 return _this; 9855 } 9856 9857 /** 9858 * A special function to trigger source set in a way that will allow player 9859 * to re-trigger if the player or tech are not ready yet. 9860 * 9861 * @fires Tech#sourceset 9862 * @param {string} src The source string at the time of the source changing. 9863 */ 9864 9865 9866 Tech.prototype.triggerSourceset = function triggerSourceset(src) { 9867 var _this2 = this; 9868 9869 if (!this.isReady_) { 9870 // on initial ready we have to trigger source set 9871 // 1ms after ready so that player can watch for it. 9872 this.one('ready', function () { 9873 return _this2.setTimeout(function () { 9874 return _this2.triggerSourceset(src); 9875 }, 1); 9876 }); 9877 } 9878 9879 /** 9880 * Fired when the source is set on the tech causing the media element 9881 * to reload. 9882 * 9883 * @see {@link Player#event:sourceset} 9884 * @event Tech#sourceset 9885 * @type {EventTarget~Event} 9886 */ 9887 this.trigger({ 9888 src: src, 9889 type: 'sourceset' 9890 }); 9891 }; 9892 9893 /* Fallbacks for unsupported event types 9894 ================================================================================ */ 9895 9896 /** 9897 * Polyfill the `progress` event for browsers that don't support it natively. 9898 * 9899 * @see {@link Tech#trackProgress} 9900 */ 9901 9902 9903 Tech.prototype.manualProgressOn = function manualProgressOn() { 9904 this.on('durationchange', this.onDurationChange); 9905 9906 this.manualProgress = true; 9907 9908 // Trigger progress watching when a source begins loading 9909 this.one('ready', this.trackProgress); 9910 }; 9911 9912 /** 9913 * Turn off the polyfill for `progress` events that was created in 9914 * {@link Tech#manualProgressOn} 9915 */ 9916 9917 9918 Tech.prototype.manualProgressOff = function manualProgressOff() { 9919 this.manualProgress = false; 9920 this.stopTrackingProgress(); 9921 9922 this.off('durationchange', this.onDurationChange); 9923 }; 9924 9925 /** 9926 * This is used to trigger a `progress` event when the buffered percent changes. It 9927 * sets an interval function that will be called every 500 milliseconds to check if the 9928 * buffer end percent has changed. 9929 * 9930 * > This function is called by {@link Tech#manualProgressOn} 9931 * 9932 * @param {EventTarget~Event} event 9933 * The `ready` event that caused this to run. 9934 * 9935 * @listens Tech#ready 9936 * @fires Tech#progress 9937 */ 9938 9939 9940 Tech.prototype.trackProgress = function trackProgress(event) { 9941 this.stopTrackingProgress(); 9942 this.progressInterval = this.setInterval(bind(this, function () { 9943 // Don't trigger unless buffered amount is greater than last time 9944 9945 var numBufferedPercent = this.bufferedPercent(); 9946 9947 if (this.bufferedPercent_ !== numBufferedPercent) { 9948 /** 9949 * See {@link Player#progress} 9950 * 9951 * @event Tech#progress 9952 * @type {EventTarget~Event} 9953 */ 9954 this.trigger('progress'); 9955 } 9956 9957 this.bufferedPercent_ = numBufferedPercent; 9958 9959 if (numBufferedPercent === 1) { 9960 this.stopTrackingProgress(); 9961 } 9962 }), 500); 9963 }; 9964 9965 /** 9966 * Update our internal duration on a `durationchange` event by calling 9967 * {@link Tech#duration}. 9968 * 9969 * @param {EventTarget~Event} event 9970 * The `durationchange` event that caused this to run. 9971 * 9972 * @listens Tech#durationchange 9973 */ 9974 9975 9976 Tech.prototype.onDurationChange = function onDurationChange(event) { 9977 this.duration_ = this.duration(); 9978 }; 9979 9980 /** 9981 * Get and create a `TimeRange` object for buffering. 9982 * 9983 * @return {TimeRange} 9984 * The time range object that was created. 9985 */ 9986 9987 9988 Tech.prototype.buffered = function buffered() { 9989 return createTimeRanges(0, 0); 9990 }; 9991 9992 /** 9993 * Get the percentage of the current video that is currently buffered. 9994 * 9995 * @return {number} 9996 * A number from 0 to 1 that represents the decimal percentage of the 9997 * video that is buffered. 9998 * 9999 */ 10000 10001 10002 Tech.prototype.bufferedPercent = function bufferedPercent$$1() { 10003 return bufferedPercent(this.buffered(), this.duration_); 10004 }; 10005 10006 /** 10007 * Turn off the polyfill for `progress` events that was created in 10008 * {@link Tech#manualProgressOn} 10009 * Stop manually tracking progress events by clearing the interval that was set in 10010 * {@link Tech#trackProgress}. 10011 */ 10012 10013 10014 Tech.prototype.stopTrackingProgress = function stopTrackingProgress() { 10015 this.clearInterval(this.progressInterval); 10016 }; 10017 10018 /** 10019 * Polyfill the `timeupdate` event for browsers that don't support it. 10020 * 10021 * @see {@link Tech#trackCurrentTime} 10022 */ 10023 10024 10025 Tech.prototype.manualTimeUpdatesOn = function manualTimeUpdatesOn() { 10026 this.manualTimeUpdates = true; 10027 10028 this.on('play', this.trackCurrentTime); 10029 this.on('pause', this.stopTrackingCurrentTime); 10030 }; 10031 10032 /** 10033 * Turn off the polyfill for `timeupdate` events that was created in 10034 * {@link Tech#manualTimeUpdatesOn} 10035 */ 10036 10037 10038 Tech.prototype.manualTimeUpdatesOff = function manualTimeUpdatesOff() { 10039 this.manualTimeUpdates = false; 10040 this.stopTrackingCurrentTime(); 10041 this.off('play', this.trackCurrentTime); 10042 this.off('pause', this.stopTrackingCurrentTime); 10043 }; 10044 10045 /** 10046 * Sets up an interval function to track current time and trigger `timeupdate` every 10047 * 250 milliseconds. 10048 * 10049 * @listens Tech#play 10050 * @triggers Tech#timeupdate 10051 */ 10052 10053 10054 Tech.prototype.trackCurrentTime = function trackCurrentTime() { 10055 if (this.currentTimeInterval) { 10056 this.stopTrackingCurrentTime(); 10057 } 10058 this.currentTimeInterval = this.setInterval(function () { 10059 /** 10060 * Triggered at an interval of 250ms to indicated that time is passing in the video. 10061 * 10062 * @event Tech#timeupdate 10063 * @type {EventTarget~Event} 10064 */ 10065 this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 10066 10067 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 10068 }, 250); 10069 }; 10070 10071 /** 10072 * Stop the interval function created in {@link Tech#trackCurrentTime} so that the 10073 * `timeupdate` event is no longer triggered. 10074 * 10075 * @listens {Tech#pause} 10076 */ 10077 10078 10079 Tech.prototype.stopTrackingCurrentTime = function stopTrackingCurrentTime() { 10080 this.clearInterval(this.currentTimeInterval); 10081 10082 // #1002 - if the video ends right before the next timeupdate would happen, 10083 // the progress bar won't make it all the way to the end 10084 this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 10085 }; 10086 10087 /** 10088 * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, 10089 * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. 10090 * 10091 * @fires Component#dispose 10092 */ 10093 10094 10095 Tech.prototype.dispose = function dispose() { 10096 10097 // clear out all tracks because we can't reuse them between techs 10098 this.clearTracks(NORMAL.names); 10099 10100 // Turn off any manual progress or timeupdate tracking 10101 if (this.manualProgress) { 10102 this.manualProgressOff(); 10103 } 10104 10105 if (this.manualTimeUpdates) { 10106 this.manualTimeUpdatesOff(); 10107 } 10108 10109 _Component.prototype.dispose.call(this); 10110 }; 10111 10112 /** 10113 * Clear out a single `TrackList` or an array of `TrackLists` given their names. 10114 * 10115 * > Note: Techs without source handlers should call this between sources for `video` 10116 * & `audio` tracks. You don't want to use them between tracks! 10117 * 10118 * @param {string[]|string} types 10119 * TrackList names to clear, valid names are `video`, `audio`, and 10120 * `text`. 10121 */ 10122 10123 10124 Tech.prototype.clearTracks = function clearTracks(types) { 10125 var _this3 = this; 10126 10127 types = [].concat(types); 10128 // clear out all tracks because we can't reuse them between techs 10129 types.forEach(function (type) { 10130 var list = _this3[type + 'Tracks']() || []; 10131 var i = list.length; 10132 10133 while (i--) { 10134 var track = list[i]; 10135 10136 if (type === 'text') { 10137 _this3.removeRemoteTextTrack(track); 10138 } 10139 list.removeTrack(track); 10140 } 10141 }); 10142 }; 10143 10144 /** 10145 * Remove any TextTracks added via addRemoteTextTrack that are 10146 * flagged for automatic garbage collection 10147 */ 10148 10149 10150 Tech.prototype.cleanupAutoTextTracks = function cleanupAutoTextTracks() { 10151 var list = this.autoRemoteTextTracks_ || []; 10152 var i = list.length; 10153 10154 while (i--) { 10155 var track = list[i]; 10156 10157 this.removeRemoteTextTrack(track); 10158 } 10159 }; 10160 10161 /** 10162 * Reset the tech, which will removes all sources and reset the internal readyState. 10163 * 10164 * @abstract 10165 */ 10166 10167 10168 Tech.prototype.reset = function reset() {}; 10169 10170 /** 10171 * Get or set an error on the Tech. 10172 * 10173 * @param {MediaError} [err] 10174 * Error to set on the Tech 10175 * 10176 * @return {MediaError|null} 10177 * The current error object on the tech, or null if there isn't one. 10178 */ 10179 10180 10181 Tech.prototype.error = function error(err) { 10182 if (err !== undefined) { 10183 this.error_ = new MediaError(err); 10184 this.trigger('error'); 10185 } 10186 return this.error_; 10187 }; 10188 10189 /** 10190 * Returns the `TimeRange`s that have been played through for the current source. 10191 * 10192 * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. 10193 * It only checks whether the source has played at all or not. 10194 * 10195 * @return {TimeRange} 10196 * - A single time range if this video has played 10197 * - An empty set of ranges if not. 10198 */ 10199 10200 10201 Tech.prototype.played = function played() { 10202 if (this.hasStarted_) { 10203 return createTimeRanges(0, 0); 10204 } 10205 return createTimeRanges(); 10206 }; 10207 10208 /** 10209 * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was 10210 * previously called. 10211 * 10212 * @fires Tech#timeupdate 10213 */ 10214 10215 10216 Tech.prototype.setCurrentTime = function setCurrentTime() { 10217 // improve the accuracy of manual timeupdates 10218 if (this.manualTimeUpdates) { 10219 /** 10220 * A manual `timeupdate` event. 10221 * 10222 * @event Tech#timeupdate 10223 * @type {EventTarget~Event} 10224 */ 10225 this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 10226 } 10227 }; 10228 10229 /** 10230 * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and 10231 * {@link TextTrackList} events. 10232 * 10233 * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. 10234 * 10235 * @fires Tech#audiotrackchange 10236 * @fires Tech#videotrackchange 10237 * @fires Tech#texttrackchange 10238 */ 10239 10240 10241 Tech.prototype.initTrackListeners = function initTrackListeners() { 10242 var _this4 = this; 10243 10244 /** 10245 * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} 10246 * 10247 * @event Tech#audiotrackchange 10248 * @type {EventTarget~Event} 10249 */ 10250 10251 /** 10252 * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} 10253 * 10254 * @event Tech#videotrackchange 10255 * @type {EventTarget~Event} 10256 */ 10257 10258 /** 10259 * Triggered when tracks are added or removed on the Tech {@link TextTrackList} 10260 * 10261 * @event Tech#texttrackchange 10262 * @type {EventTarget~Event} 10263 */ 10264 NORMAL.names.forEach(function (name) { 10265 var props = NORMAL[name]; 10266 var trackListChanges = function trackListChanges() { 10267 _this4.trigger(name + 'trackchange'); 10268 }; 10269 10270 var tracks = _this4[props.getterName](); 10271 10272 tracks.addEventListener('removetrack', trackListChanges); 10273 tracks.addEventListener('addtrack', trackListChanges); 10274 10275 _this4.on('dispose', function () { 10276 tracks.removeEventListener('removetrack', trackListChanges); 10277 tracks.removeEventListener('addtrack', trackListChanges); 10278 }); 10279 }); 10280 }; 10281 10282 /** 10283 * Emulate TextTracks using vtt.js if necessary 10284 * 10285 * @fires Tech#vttjsloaded 10286 * @fires Tech#vttjserror 10287 */ 10288 10289 10290 Tech.prototype.addWebVttScript_ = function addWebVttScript_() { 10291 var _this5 = this; 10292 10293 if (window_1.WebVTT) { 10294 return; 10295 } 10296 10297 // Initially, Tech.el_ is a child of a dummy-div wait until the Component system 10298 // signals that the Tech is ready at which point Tech.el_ is part of the DOM 10299 // before inserting the WebVTT script 10300 if (document_1.body.contains(this.el())) { 10301 10302 // load via require if available and vtt.js script location was not passed in 10303 // as an option. novtt builds will turn the above require call into an empty object 10304 // which will cause this if check to always fail. 10305 if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) { 10306 this.trigger('vttjsloaded'); 10307 return; 10308 } 10309 10310 // load vtt.js via the script location option or the cdn of no location was 10311 // passed in 10312 var script = document_1.createElement('script'); 10313 10314 script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; 10315 script.onload = function () { 10316 /** 10317 * Fired when vtt.js is loaded. 10318 * 10319 * @event Tech#vttjsloaded 10320 * @type {EventTarget~Event} 10321 */ 10322 _this5.trigger('vttjsloaded'); 10323 }; 10324 script.onerror = function () { 10325 /** 10326 * Fired when vtt.js was not loaded due to an error 10327 * 10328 * @event Tech#vttjsloaded 10329 * @type {EventTarget~Event} 10330 */ 10331 _this5.trigger('vttjserror'); 10332 }; 10333 this.on('dispose', function () { 10334 script.onload = null; 10335 script.onerror = null; 10336 }); 10337 // but have not loaded yet and we set it to true before the inject so that 10338 // we don't overwrite the injected window.WebVTT if it loads right away 10339 window_1.WebVTT = true; 10340 this.el().parentNode.appendChild(script); 10341 } else { 10342 this.ready(this.addWebVttScript_); 10343 } 10344 }; 10345 10346 /** 10347 * Emulate texttracks 10348 * 10349 */ 10350 10351 10352 Tech.prototype.emulateTextTracks = function emulateTextTracks() { 10353 var _this6 = this; 10354 10355 var tracks = this.textTracks(); 10356 var remoteTracks = this.remoteTextTracks(); 10357 var handleAddTrack = function handleAddTrack(e) { 10358 return tracks.addTrack(e.track); 10359 }; 10360 var handleRemoveTrack = function handleRemoveTrack(e) { 10361 return tracks.removeTrack(e.track); 10362 }; 10363 10364 remoteTracks.on('addtrack', handleAddTrack); 10365 remoteTracks.on('removetrack', handleRemoveTrack); 10366 10367 this.addWebVttScript_(); 10368 10369 var updateDisplay = function updateDisplay() { 10370 return _this6.trigger('texttrackchange'); 10371 }; 10372 10373 var textTracksChanges = function textTracksChanges() { 10374 updateDisplay(); 10375 10376 for (var i = 0; i < tracks.length; i++) { 10377 var track = tracks[i]; 10378 10379 track.removeEventListener('cuechange', updateDisplay); 10380 if (track.mode === 'showing') { 10381 track.addEventListener('cuechange', updateDisplay); 10382 } 10383 } 10384 }; 10385 10386 textTracksChanges(); 10387 tracks.addEventListener('change', textTracksChanges); 10388 tracks.addEventListener('addtrack', textTracksChanges); 10389 tracks.addEventListener('removetrack', textTracksChanges); 10390 10391 this.on('dispose', function () { 10392 remoteTracks.off('addtrack', handleAddTrack); 10393 remoteTracks.off('removetrack', handleRemoveTrack); 10394 tracks.removeEventListener('change', textTracksChanges); 10395 tracks.removeEventListener('addtrack', textTracksChanges); 10396 tracks.removeEventListener('removetrack', textTracksChanges); 10397 10398 for (var i = 0; i < tracks.length; i++) { 10399 var track = tracks[i]; 10400 10401 track.removeEventListener('cuechange', updateDisplay); 10402 } 10403 }); 10404 }; 10405 10406 /** 10407 * Create and returns a remote {@link TextTrack} object. 10408 * 10409 * @param {string} kind 10410 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 10411 * 10412 * @param {string} [label] 10413 * Label to identify the text track 10414 * 10415 * @param {string} [language] 10416 * Two letter language abbreviation 10417 * 10418 * @return {TextTrack} 10419 * The TextTrack that gets created. 10420 */ 10421 10422 10423 Tech.prototype.addTextTrack = function addTextTrack(kind, label, language) { 10424 if (!kind) { 10425 throw new Error('TextTrack kind is required but was not provided'); 10426 } 10427 10428 return createTrackHelper(this, kind, label, language); 10429 }; 10430 10431 /** 10432 * Create an emulated TextTrack for use by addRemoteTextTrack 10433 * 10434 * This is intended to be overridden by classes that inherit from 10435 * Tech in order to create native or custom TextTracks. 10436 * 10437 * @param {Object} options 10438 * The object should contain the options to initialize the TextTrack with. 10439 * 10440 * @param {string} [options.kind] 10441 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 10442 * 10443 * @param {string} [options.label]. 10444 * Label to identify the text track 10445 * 10446 * @param {string} [options.language] 10447 * Two letter language abbreviation. 10448 * 10449 * @return {HTMLTrackElement} 10450 * The track element that gets created. 10451 */ 10452 10453 10454 Tech.prototype.createRemoteTextTrack = function createRemoteTextTrack(options) { 10455 var track = mergeOptions(options, { 10456 tech: this 10457 }); 10458 10459 return new REMOTE.remoteTextEl.TrackClass(track); 10460 }; 10461 10462 /** 10463 * Creates a remote text track object and returns an html track element. 10464 * 10465 * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. 10466 * 10467 * @param {Object} options 10468 * See {@link Tech#createRemoteTextTrack} for more detailed properties. 10469 * 10470 * @param {boolean} [manualCleanup=true] 10471 * - When false: the TextTrack will be automatically removed from the video 10472 * element whenever the source changes 10473 * - When True: The TextTrack will have to be cleaned up manually 10474 * 10475 * @return {HTMLTrackElement} 10476 * An Html Track Element. 10477 * 10478 * @deprecated The default functionality for this function will be equivalent 10479 * to "manualCleanup=false" in the future. The manualCleanup parameter will 10480 * also be removed. 10481 */ 10482 10483 10484 Tech.prototype.addRemoteTextTrack = function addRemoteTextTrack() { 10485 var _this7 = this; 10486 10487 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 10488 var manualCleanup = arguments[1]; 10489 10490 var htmlTrackElement = this.createRemoteTextTrack(options); 10491 10492 if (manualCleanup !== true && manualCleanup !== false) { 10493 // deprecation warning 10494 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'); 10495 manualCleanup = true; 10496 } 10497 10498 // store HTMLTrackElement and TextTrack to remote list 10499 this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); 10500 this.remoteTextTracks().addTrack(htmlTrackElement.track); 10501 10502 if (manualCleanup !== true) { 10503 // create the TextTrackList if it doesn't exist 10504 this.ready(function () { 10505 return _this7.autoRemoteTextTracks_.addTrack(htmlTrackElement.track); 10506 }); 10507 } 10508 10509 return htmlTrackElement; 10510 }; 10511 10512 /** 10513 * Remove a remote text track from the remote `TextTrackList`. 10514 * 10515 * @param {TextTrack} track 10516 * `TextTrack` to remove from the `TextTrackList` 10517 */ 10518 10519 10520 Tech.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { 10521 var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); 10522 10523 // remove HTMLTrackElement and TextTrack from remote list 10524 this.remoteTextTrackEls().removeTrackElement_(trackElement); 10525 this.remoteTextTracks().removeTrack(track); 10526 this.autoRemoteTextTracks_.removeTrack(track); 10527 }; 10528 10529 /** 10530 * Gets available media playback quality metrics as specified by the W3C's Media 10531 * Playback Quality API. 10532 * 10533 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 10534 * 10535 * @return {Object} 10536 * An object with supported media playback quality metrics 10537 * 10538 * @abstract 10539 */ 10540 10541 10542 Tech.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 10543 return {}; 10544 }; 10545 10546 /** 10547 * A method to set a poster from a `Tech`. 10548 * 10549 * @abstract 10550 */ 10551 10552 10553 Tech.prototype.setPoster = function setPoster() {}; 10554 10555 /** 10556 * A method to check for the presence of the 'playsinline' <video> attribute. 10557 * 10558 * @abstract 10559 */ 10560 10561 10562 Tech.prototype.playsinline = function playsinline() {}; 10563 10564 /** 10565 * A method to set or unset the 'playsinline' <video> attribute. 10566 * 10567 * @abstract 10568 */ 10569 10570 10571 Tech.prototype.setPlaysinline = function setPlaysinline() {}; 10572 10573 /** 10574 * Attempt to force override of native audio tracks. 10575 * 10576 * @param {Boolean} override - If set to true native audio will be overridden, 10577 * otherwise native audio will potentially be used. 10578 * 10579 * @abstract 10580 */ 10581 10582 10583 Tech.prototype.overrideNativeAudioTracks = function overrideNativeAudioTracks() {}; 10584 10585 /** 10586 * Attempt to force override of native video tracks. 10587 * 10588 * @param {Boolean} override - If set to true native video will be overridden, 10589 * otherwise native video will potentially be used. 10590 * 10591 * @abstract 10592 */ 10593 10594 10595 Tech.prototype.overrideNativeVideoTracks = function overrideNativeVideoTracks() {}; 10596 10597 /* 10598 * Check if the tech can support the given mime-type. 10599 * 10600 * The base tech does not support any type, but source handlers might 10601 * overwrite this. 10602 * 10603 * @param {string} type 10604 * The mimetype to check for support 10605 * 10606 * @return {string} 10607 * 'probably', 'maybe', or empty string 10608 * 10609 * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType} 10610 * 10611 * @abstract 10612 */ 10613 10614 10615 Tech.prototype.canPlayType = function canPlayType() { 10616 return ''; 10617 }; 10618 10619 /** 10620 * Check if the type is supported by this tech. 10621 * 10622 * The base tech does not support any type, but source handlers might 10623 * overwrite this. 10624 * 10625 * @param {string} type 10626 * The media type to check 10627 * @return {string} Returns the native video element's response 10628 */ 10629 10630 10631 Tech.canPlayType = function canPlayType() { 10632 return ''; 10633 }; 10634 10635 /** 10636 * Check if the tech can support the given source 10637 * @param {Object} srcObj 10638 * The source object 10639 * @param {Object} options 10640 * The options passed to the tech 10641 * @return {string} 'probably', 'maybe', or '' (empty string) 10642 */ 10643 10644 10645 Tech.canPlaySource = function canPlaySource(srcObj, options) { 10646 return Tech.canPlayType(srcObj.type); 10647 }; 10648 10649 /* 10650 * Return whether the argument is a Tech or not. 10651 * Can be passed either a Class like `Html5` or a instance like `player.tech_` 10652 * 10653 * @param {Object} component 10654 * The item to check 10655 * 10656 * @return {boolean} 10657 * Whether it is a tech or not 10658 * - True if it is a tech 10659 * - False if it is not 10660 */ 10661 10662 10663 Tech.isTech = function isTech(component) { 10664 return component.prototype instanceof Tech || component instanceof Tech || component === Tech; 10665 }; 10666 10667 /** 10668 * Registers a `Tech` into a shared list for videojs. 10669 * 10670 * @param {string} name 10671 * Name of the `Tech` to register. 10672 * 10673 * @param {Object} tech 10674 * The `Tech` class to register. 10675 */ 10676 10677 10678 Tech.registerTech = function registerTech(name, tech) { 10679 if (!Tech.techs_) { 10680 Tech.techs_ = {}; 10681 } 10682 10683 if (!Tech.isTech(tech)) { 10684 throw new Error('Tech ' + name + ' must be a Tech'); 10685 } 10686 10687 if (!Tech.canPlayType) { 10688 throw new Error('Techs must have a static canPlayType method on them'); 10689 } 10690 if (!Tech.canPlaySource) { 10691 throw new Error('Techs must have a static canPlaySource method on them'); 10692 } 10693 10694 name = toTitleCase(name); 10695 10696 Tech.techs_[name] = tech; 10697 if (name !== 'Tech') { 10698 // camel case the techName for use in techOrder 10699 Tech.defaultTechOrder_.push(name); 10700 } 10701 return tech; 10702 }; 10703 10704 /** 10705 * Get a `Tech` from the shared list by name. 10706 * 10707 * @param {string} name 10708 * `camelCase` or `TitleCase` name of the Tech to get 10709 * 10710 * @return {Tech|undefined} 10711 * The `Tech` or undefined if there was no tech with the name requested. 10712 */ 10713 10714 10715 Tech.getTech = function getTech(name) { 10716 if (!name) { 10717 return; 10718 } 10719 10720 name = toTitleCase(name); 10721 10722 if (Tech.techs_ && Tech.techs_[name]) { 10723 return Tech.techs_[name]; 10724 } 10725 10726 if (window_1 && window_1.videojs && window_1.videojs[name]) { 10727 log$1.warn('The ' + name + ' tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)'); 10728 return window_1.videojs[name]; 10729 } 10730 }; 10731 10732 return Tech; 10733 }(Component); 10734 10735 /** 10736 * Get the {@link VideoTrackList} 10737 * 10738 * @returns {VideoTrackList} 10739 * @method Tech.prototype.videoTracks 10740 */ 10741 10742 /** 10743 * Get the {@link AudioTrackList} 10744 * 10745 * @returns {AudioTrackList} 10746 * @method Tech.prototype.audioTracks 10747 */ 10748 10749 /** 10750 * Get the {@link TextTrackList} 10751 * 10752 * @returns {TextTrackList} 10753 * @method Tech.prototype.textTracks 10754 */ 10755 10756 /** 10757 * Get the remote element {@link TextTrackList} 10758 * 10759 * @returns {TextTrackList} 10760 * @method Tech.prototype.remoteTextTracks 10761 */ 10762 10763 /** 10764 * Get the remote element {@link HtmlTrackElementList} 10765 * 10766 * @returns {HtmlTrackElementList} 10767 * @method Tech.prototype.remoteTextTrackEls 10768 */ 10769 10770 ALL.names.forEach(function (name) { 10771 var props = ALL[name]; 10772 10773 Tech.prototype[props.getterName] = function () { 10774 this[props.privateName] = this[props.privateName] || new props.ListClass(); 10775 return this[props.privateName]; 10776 }; 10777 }); 10778 10779 /** 10780 * List of associated text tracks 10781 * 10782 * @type {TextTrackList} 10783 * @private 10784 * @property Tech#textTracks_ 10785 */ 10786 10787 /** 10788 * List of associated audio tracks. 10789 * 10790 * @type {AudioTrackList} 10791 * @private 10792 * @property Tech#audioTracks_ 10793 */ 10794 10795 /** 10796 * List of associated video tracks. 10797 * 10798 * @type {VideoTrackList} 10799 * @private 10800 * @property Tech#videoTracks_ 10801 */ 10802 10803 /** 10804 * Boolean indicating whether the `Tech` supports volume control. 10805 * 10806 * @type {boolean} 10807 * @default 10808 */ 10809 Tech.prototype.featuresVolumeControl = true; 10810 10811 /** 10812 * Boolean indicating whether the `Tech` supports fullscreen resize control. 10813 * Resizing plugins using request fullscreen reloads the plugin 10814 * 10815 * @type {boolean} 10816 * @default 10817 */ 10818 Tech.prototype.featuresFullscreenResize = false; 10819 10820 /** 10821 * Boolean indicating whether the `Tech` supports changing the speed at which the video 10822 * plays. Examples: 10823 * - Set player to play 2x (twice) as fast 10824 * - Set player to play 0.5x (half) as fast 10825 * 10826 * @type {boolean} 10827 * @default 10828 */ 10829 Tech.prototype.featuresPlaybackRate = false; 10830 10831 /** 10832 * Boolean indicating whether the `Tech` supports the `progress` event. This is currently 10833 * not triggered by video-js-swf. This will be used to determine if 10834 * {@link Tech#manualProgressOn} should be called. 10835 * 10836 * @type {boolean} 10837 * @default 10838 */ 10839 Tech.prototype.featuresProgressEvents = false; 10840 10841 /** 10842 * Boolean indicating whether the `Tech` supports the `sourceset` event. 10843 * 10844 * A tech should set this to `true` and then use {@link Tech#triggerSourceset} 10845 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting 10846 * a new source. 10847 * 10848 * @type {boolean} 10849 * @default 10850 */ 10851 Tech.prototype.featuresSourceset = false; 10852 10853 /** 10854 * Boolean indicating whether the `Tech` supports the `timeupdate` event. This is currently 10855 * not triggered by video-js-swf. This will be used to determine if 10856 * {@link Tech#manualTimeUpdates} should be called. 10857 * 10858 * @type {boolean} 10859 * @default 10860 */ 10861 Tech.prototype.featuresTimeupdateEvents = false; 10862 10863 /** 10864 * Boolean indicating whether the `Tech` supports the native `TextTrack`s. 10865 * This will help us integrate with native `TextTrack`s if the browser supports them. 10866 * 10867 * @type {boolean} 10868 * @default 10869 */ 10870 Tech.prototype.featuresNativeTextTracks = false; 10871 10872 /** 10873 * A functional mixin for techs that want to use the Source Handler pattern. 10874 * Source handlers are scripts for handling specific formats. 10875 * The source handler pattern is used for adaptive formats (HLS, DASH) that 10876 * manually load video data and feed it into a Source Buffer (Media Source Extensions) 10877 * Example: `Tech.withSourceHandlers.call(MyTech);` 10878 * 10879 * @param {Tech} _Tech 10880 * The tech to add source handler functions to. 10881 * 10882 * @mixes Tech~SourceHandlerAdditions 10883 */ 10884 Tech.withSourceHandlers = function (_Tech) { 10885 10886 /** 10887 * Register a source handler 10888 * 10889 * @param {Function} handler 10890 * The source handler class 10891 * 10892 * @param {number} [index] 10893 * Register it at the following index 10894 */ 10895 _Tech.registerSourceHandler = function (handler, index) { 10896 var handlers = _Tech.sourceHandlers; 10897 10898 if (!handlers) { 10899 handlers = _Tech.sourceHandlers = []; 10900 } 10901 10902 if (index === undefined) { 10903 // add to the end of the list 10904 index = handlers.length; 10905 } 10906 10907 handlers.splice(index, 0, handler); 10908 }; 10909 10910 /** 10911 * Check if the tech can support the given type. Also checks the 10912 * Techs sourceHandlers. 10913 * 10914 * @param {string} type 10915 * The mimetype to check. 10916 * 10917 * @return {string} 10918 * 'probably', 'maybe', or '' (empty string) 10919 */ 10920 _Tech.canPlayType = function (type) { 10921 var handlers = _Tech.sourceHandlers || []; 10922 var can = void 0; 10923 10924 for (var i = 0; i < handlers.length; i++) { 10925 can = handlers[i].canPlayType(type); 10926 10927 if (can) { 10928 return can; 10929 } 10930 } 10931 10932 return ''; 10933 }; 10934 10935 /** 10936 * Returns the first source handler that supports the source. 10937 * 10938 * TODO: Answer question: should 'probably' be prioritized over 'maybe' 10939 * 10940 * @param {Tech~SourceObject} source 10941 * The source object 10942 * 10943 * @param {Object} options 10944 * The options passed to the tech 10945 * 10946 * @return {SourceHandler|null} 10947 * The first source handler that supports the source or null if 10948 * no SourceHandler supports the source 10949 */ 10950 _Tech.selectSourceHandler = function (source, options) { 10951 var handlers = _Tech.sourceHandlers || []; 10952 var can = void 0; 10953 10954 for (var i = 0; i < handlers.length; i++) { 10955 can = handlers[i].canHandleSource(source, options); 10956 10957 if (can) { 10958 return handlers[i]; 10959 } 10960 } 10961 10962 return null; 10963 }; 10964 10965 /** 10966 * Check if the tech can support the given source. 10967 * 10968 * @param {Tech~SourceObject} srcObj 10969 * The source object 10970 * 10971 * @param {Object} options 10972 * The options passed to the tech 10973 * 10974 * @return {string} 10975 * 'probably', 'maybe', or '' (empty string) 10976 */ 10977 _Tech.canPlaySource = function (srcObj, options) { 10978 var sh = _Tech.selectSourceHandler(srcObj, options); 10979 10980 if (sh) { 10981 return sh.canHandleSource(srcObj, options); 10982 } 10983 10984 return ''; 10985 }; 10986 10987 /** 10988 * When using a source handler, prefer its implementation of 10989 * any function normally provided by the tech. 10990 */ 10991 var deferrable = ['seekable', 'seeking', 'duration']; 10992 10993 /** 10994 * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable 10995 * function if it exists, with a fallback to the Techs seekable function. 10996 * 10997 * @method _Tech.seekable 10998 */ 10999 11000 /** 11001 * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration 11002 * function if it exists, otherwise it will fallback to the techs duration function. 11003 * 11004 * @method _Tech.duration 11005 */ 11006 11007 deferrable.forEach(function (fnName) { 11008 var originalFn = this[fnName]; 11009 11010 if (typeof originalFn !== 'function') { 11011 return; 11012 } 11013 11014 this[fnName] = function () { 11015 if (this.sourceHandler_ && this.sourceHandler_[fnName]) { 11016 return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); 11017 } 11018 return originalFn.apply(this, arguments); 11019 }; 11020 }, _Tech.prototype); 11021 11022 /** 11023 * Create a function for setting the source using a source object 11024 * and source handlers. 11025 * Should never be called unless a source handler was found. 11026 * 11027 * @param {Tech~SourceObject} source 11028 * A source object with src and type keys 11029 */ 11030 _Tech.prototype.setSource = function (source) { 11031 var sh = _Tech.selectSourceHandler(source, this.options_); 11032 11033 if (!sh) { 11034 // Fall back to a native source hander when unsupported sources are 11035 // deliberately set 11036 if (_Tech.nativeSourceHandler) { 11037 sh = _Tech.nativeSourceHandler; 11038 } else { 11039 log$1.error('No source handler found for the current source.'); 11040 } 11041 } 11042 11043 // Dispose any existing source handler 11044 this.disposeSourceHandler(); 11045 this.off('dispose', this.disposeSourceHandler); 11046 11047 if (sh !== _Tech.nativeSourceHandler) { 11048 this.currentSource_ = source; 11049 } 11050 11051 this.sourceHandler_ = sh.handleSource(source, this, this.options_); 11052 this.on('dispose', this.disposeSourceHandler); 11053 }; 11054 11055 /** 11056 * Clean up any existing SourceHandlers and listeners when the Tech is disposed. 11057 * 11058 * @listens Tech#dispose 11059 */ 11060 _Tech.prototype.disposeSourceHandler = function () { 11061 // if we have a source and get another one 11062 // then we are loading something new 11063 // than clear all of our current tracks 11064 if (this.currentSource_) { 11065 this.clearTracks(['audio', 'video']); 11066 this.currentSource_ = null; 11067 } 11068 11069 // always clean up auto-text tracks 11070 this.cleanupAutoTextTracks(); 11071 11072 if (this.sourceHandler_) { 11073 11074 if (this.sourceHandler_.dispose) { 11075 this.sourceHandler_.dispose(); 11076 } 11077 11078 this.sourceHandler_ = null; 11079 } 11080 }; 11081 }; 11082 11083 // The base Tech class needs to be registered as a Component. It is the only 11084 // Tech that can be registered as a Component. 11085 Component.registerComponent('Tech', Tech); 11086 Tech.registerTech('Tech', Tech); 11087 11088 /** 11089 * A list of techs that should be added to techOrder on Players 11090 * 11091 * @private 11092 */ 11093 Tech.defaultTechOrder_ = []; 11094 11095 var middlewares = {}; 11096 var middlewareInstances = {}; 11097 11098 var TERMINATOR = {}; 11099 11100 function use(type, middleware) { 11101 middlewares[type] = middlewares[type] || []; 11102 middlewares[type].push(middleware); 11103 } 11104 11105 function setSource(player, src, next) { 11106 player.setTimeout(function () { 11107 return setSourceHelper(src, middlewares[src.type], next, player); 11108 }, 1); 11109 } 11110 11111 function setTech(middleware, tech) { 11112 middleware.forEach(function (mw) { 11113 return mw.setTech && mw.setTech(tech); 11114 }); 11115 } 11116 11117 /** 11118 * Calls a getter on the tech first, through each middleware 11119 * from right to left to the player. 11120 */ 11121 function get$1(middleware, tech, method) { 11122 return middleware.reduceRight(middlewareIterator(method), tech[method]()); 11123 } 11124 11125 /** 11126 * Takes the argument given to the player and calls the setter method on each 11127 * middleware from left to right to the tech. 11128 */ 11129 function set$1(middleware, tech, method, arg) { 11130 return tech[method](middleware.reduce(middlewareIterator(method), arg)); 11131 } 11132 11133 /** 11134 * Takes the argument given to the player and calls the `call` version of the method 11135 * on each middleware from left to right. 11136 * Then, call the passed in method on the tech and return the result unchanged 11137 * back to the player, through middleware, this time from right to left. 11138 */ 11139 function mediate(middleware, tech, method) { 11140 var arg = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; 11141 11142 var callMethod = 'call' + toTitleCase(method); 11143 var middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg); 11144 var terminated = middlewareValue === TERMINATOR; 11145 var returnValue = terminated ? null : tech[method](middlewareValue); 11146 11147 executeRight(middleware, method, returnValue, terminated); 11148 11149 return returnValue; 11150 } 11151 11152 var allowedGetters = { 11153 buffered: 1, 11154 currentTime: 1, 11155 duration: 1, 11156 seekable: 1, 11157 played: 1, 11158 paused: 1 11159 }; 11160 11161 var allowedSetters = { 11162 setCurrentTime: 1 11163 }; 11164 11165 var allowedMediators = { 11166 play: 1, 11167 pause: 1 11168 }; 11169 11170 function middlewareIterator(method) { 11171 return function (value, mw) { 11172 // if the previous middleware terminated, pass along the termination 11173 if (value === TERMINATOR) { 11174 return TERMINATOR; 11175 } 11176 11177 if (mw[method]) { 11178 return mw[method](value); 11179 } 11180 11181 return value; 11182 }; 11183 } 11184 11185 function executeRight(mws, method, value, terminated) { 11186 for (var i = mws.length - 1; i >= 0; i--) { 11187 var mw = mws[i]; 11188 11189 if (mw[method]) { 11190 mw[method](terminated, value); 11191 } 11192 } 11193 } 11194 11195 function clearCacheForPlayer(player) { 11196 middlewareInstances[player.id()] = null; 11197 } 11198 11199 /** 11200 * { 11201 * [playerId]: [[mwFactory, mwInstance], ...] 11202 * } 11203 */ 11204 function getOrCreateFactory(player, mwFactory) { 11205 var mws = middlewareInstances[player.id()]; 11206 var mw = null; 11207 11208 if (mws === undefined || mws === null) { 11209 mw = mwFactory(player); 11210 middlewareInstances[player.id()] = [[mwFactory, mw]]; 11211 return mw; 11212 } 11213 11214 for (var i = 0; i < mws.length; i++) { 11215 var _mws$i = mws[i], 11216 mwf = _mws$i[0], 11217 mwi = _mws$i[1]; 11218 11219 11220 if (mwf !== mwFactory) { 11221 continue; 11222 } 11223 11224 mw = mwi; 11225 } 11226 11227 if (mw === null) { 11228 mw = mwFactory(player); 11229 mws.push([mwFactory, mw]); 11230 } 11231 11232 return mw; 11233 } 11234 11235 function setSourceHelper() { 11236 var src = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 11237 var middleware = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 11238 var next = arguments[2]; 11239 var player = arguments[3]; 11240 var acc = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; 11241 var lastRun = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false; 11242 var mwFactory = middleware[0], 11243 mwrest = middleware.slice(1); 11244 11245 // if mwFactory is a string, then we're at a fork in the road 11246 11247 if (typeof mwFactory === 'string') { 11248 setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun); 11249 11250 // if we have an mwFactory, call it with the player to get the mw, 11251 // then call the mw's setSource method 11252 } else if (mwFactory) { 11253 var mw = getOrCreateFactory(player, mwFactory); 11254 11255 mw.setSource(assign({}, src), function (err, _src) { 11256 11257 // something happened, try the next middleware on the current level 11258 // make sure to use the old src 11259 if (err) { 11260 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 11261 } 11262 11263 // we've succeeded, now we need to go deeper 11264 acc.push(mw); 11265 11266 // if it's the same type, continue down the current chain 11267 // otherwise, we want to go down the new chain 11268 setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun); 11269 }); 11270 } else if (mwrest.length) { 11271 setSourceHelper(src, mwrest, next, player, acc, lastRun); 11272 } else if (lastRun) { 11273 next(src, acc); 11274 } else { 11275 setSourceHelper(src, middlewares['*'], next, player, acc, true); 11276 } 11277 } 11278 11279 /** 11280 * Mimetypes 11281 * 11282 * @see http://hul.harvard.edu/ois/////systems/wax/wax-public-help/mimetypes.htm 11283 * @typedef Mimetypes~Kind 11284 * @enum 11285 */ 11286 var MimetypesKind = { 11287 opus: 'video/ogg', 11288 ogv: 'video/ogg', 11289 mp4: 'video/mp4', 11290 mov: 'video/mp4', 11291 m4v: 'video/mp4', 11292 mkv: 'video/x-matroska', 11293 mp3: 'audio/mpeg', 11294 aac: 'audio/aac', 11295 oga: 'audio/ogg', 11296 m3u8: 'application/x-mpegURL' 11297 }; 11298 11299 /** 11300 * Get the mimetype of a given src url if possible 11301 * 11302 * @param {string} src 11303 * The url to the src 11304 * 11305 * @return {string} 11306 * return the mimetype if it was known or empty string otherwise 11307 */ 11308 var getMimetype = function getMimetype() { 11309 var src = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 11310 11311 var ext = getFileExtension(src); 11312 var mimetype = MimetypesKind[ext.toLowerCase()]; 11313 11314 return mimetype || ''; 11315 }; 11316 11317 /** 11318 * Find the mime type of a given source string if possible. Uses the player 11319 * source cache. 11320 * 11321 * @param {Player} player 11322 * The player object 11323 * 11324 * @param {string} src 11325 * The source string 11326 * 11327 * @return {string} 11328 * The type that was found 11329 */ 11330 var findMimetype = function findMimetype(player, src) { 11331 if (!src) { 11332 return ''; 11333 } 11334 11335 // 1. check for the type in the `source` cache 11336 if (player.cache_.source.src === src && player.cache_.source.type) { 11337 return player.cache_.source.type; 11338 } 11339 11340 // 2. see if we have this source in our `currentSources` cache 11341 var matchingSources = player.cache_.sources.filter(function (s) { 11342 return s.src === src; 11343 }); 11344 11345 if (matchingSources.length) { 11346 return matchingSources[0].type; 11347 } 11348 11349 // 3. look for the src url in source elements and use the type there 11350 var sources = player.$$('source'); 11351 11352 for (var i = 0; i < sources.length; i++) { 11353 var s = sources[i]; 11354 11355 if (s.type && s.src && s.src === src) { 11356 return s.type; 11357 } 11358 } 11359 11360 // 4. finally fallback to our list of mime types based on src url extension 11361 return getMimetype(src); 11362 }; 11363 11364 /** 11365 * @module filter-source 11366 */ 11367 11368 /** 11369 * Filter out single bad source objects or multiple source objects in an 11370 * array. Also flattens nested source object arrays into a 1 dimensional 11371 * array of source objects. 11372 * 11373 * @param {Tech~SourceObject|Tech~SourceObject[]} src 11374 * The src object to filter 11375 * 11376 * @return {Tech~SourceObject[]} 11377 * An array of sourceobjects containing only valid sources 11378 * 11379 * @private 11380 */ 11381 var filterSource = function filterSource(src) { 11382 // traverse array 11383 if (Array.isArray(src)) { 11384 var newsrc = []; 11385 11386 src.forEach(function (srcobj) { 11387 srcobj = filterSource(srcobj); 11388 11389 if (Array.isArray(srcobj)) { 11390 newsrc = newsrc.concat(srcobj); 11391 } else if (isObject(srcobj)) { 11392 newsrc.push(srcobj); 11393 } 11394 }); 11395 11396 src = newsrc; 11397 } else if (typeof src === 'string' && src.trim()) { 11398 // convert string into object 11399 src = [fixSource({ src: src })]; 11400 } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) { 11401 // src is already valid 11402 src = [fixSource(src)]; 11403 } else { 11404 // invalid source, turn it into an empty array 11405 src = []; 11406 } 11407 11408 return src; 11409 }; 11410 11411 /** 11412 * Checks src mimetype, adding it when possible 11413 * 11414 * @param {Tech~SourceObject} src 11415 * The src object to check 11416 * @return {Tech~SourceObject} 11417 * src Object with known type 11418 */ 11419 function fixSource(src) { 11420 var mimetype = getMimetype(src.src); 11421 11422 if (!src.type && mimetype) { 11423 src.type = mimetype; 11424 } 11425 11426 return src; 11427 } 11428 11429 /** 11430 * @file loader.js 11431 */ 11432 11433 /** 11434 * The `MediaLoader` is the `Component` that decides which playback technology to load 11435 * when a player is initialized. 11436 * 11437 * @extends Component 11438 */ 11439 11440 var MediaLoader = function (_Component) { 11441 inherits(MediaLoader, _Component); 11442 11443 /** 11444 * Create an instance of this class. 11445 * 11446 * @param {Player} player 11447 * The `Player` that this class should attach to. 11448 * 11449 * @param {Object} [options] 11450 * The key/value store of player options. 11451 * 11452 * @param {Component~ReadyCallback} [ready] 11453 * The function that is run when this component is ready. 11454 */ 11455 function MediaLoader(player, options, ready) { 11456 classCallCheck(this, MediaLoader); 11457 11458 // MediaLoader has no element 11459 var options_ = mergeOptions({ createEl: false }, options); 11460 11461 // If there are no sources when the player is initialized, 11462 // load the first supported playback technology. 11463 11464 var _this = possibleConstructorReturn(this, _Component.call(this, player, options_, ready)); 11465 11466 if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) { 11467 for (var i = 0, j = options.playerOptions.techOrder; i < j.length; i++) { 11468 var techName = toTitleCase(j[i]); 11469 var tech = Tech.getTech(techName); 11470 11471 // Support old behavior of techs being registered as components. 11472 // Remove once that deprecated behavior is removed. 11473 if (!techName) { 11474 tech = Component.getComponent(techName); 11475 } 11476 11477 // Check if the browser supports this technology 11478 if (tech && tech.isSupported()) { 11479 player.loadTech_(techName); 11480 break; 11481 } 11482 } 11483 } else { 11484 // Loop through playback technologies (HTML5, Flash) and check for support. 11485 // Then load the best source. 11486 // A few assumptions here: 11487 // All playback technologies respect preload false. 11488 player.src(options.playerOptions.sources); 11489 } 11490 return _this; 11491 } 11492 11493 return MediaLoader; 11494 }(Component); 11495 11496 Component.registerComponent('MediaLoader', MediaLoader); 11497 11498 /** 11499 * @file clickable-component.js 11500 */ 11501 11502 /** 11503 * Clickable Component which is clickable or keyboard actionable, 11504 * but is not a native HTML button. 11505 * 11506 * @extends Component 11507 */ 11508 11509 var ClickableComponent = function (_Component) { 11510 inherits(ClickableComponent, _Component); 11511 11512 /** 11513 * Creates an instance of this class. 11514 * 11515 * @param {Player} player 11516 * The `Player` that this class should be attached to. 11517 * 11518 * @param {Object} [options] 11519 * The key/value store of player options. 11520 */ 11521 function ClickableComponent(player, options) { 11522 classCallCheck(this, ClickableComponent); 11523 11524 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 11525 11526 _this.emitTapEvents(); 11527 11528 _this.enable(); 11529 return _this; 11530 } 11531 11532 /** 11533 * Create the `Component`s DOM element. 11534 * 11535 * @param {string} [tag=div] 11536 * The element's node type. 11537 * 11538 * @param {Object} [props={}] 11539 * An object of properties that should be set on the element. 11540 * 11541 * @param {Object} [attributes={}] 11542 * An object of attributes that should be set on the element. 11543 * 11544 * @return {Element} 11545 * The element that gets created. 11546 */ 11547 11548 11549 ClickableComponent.prototype.createEl = function createEl$$1() { 11550 var tag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div'; 11551 var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 11552 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 11553 11554 props = assign({ 11555 innerHTML: '<span aria-hidden="true" class="vjs-icon-placeholder"></span>', 11556 className: this.buildCSSClass(), 11557 tabIndex: 0 11558 }, props); 11559 11560 if (tag === 'button') { 11561 log$1.error('Creating a ClickableComponent with an HTML element of ' + tag + ' is not supported; use a Button instead.'); 11562 } 11563 11564 // Add ARIA attributes for clickable element which is not a native HTML button 11565 attributes = assign({ 11566 role: 'button' 11567 }, attributes); 11568 11569 this.tabIndex_ = props.tabIndex; 11570 11571 var el = _Component.prototype.createEl.call(this, tag, props, attributes); 11572 11573 this.createControlTextEl(el); 11574 11575 return el; 11576 }; 11577 11578 ClickableComponent.prototype.dispose = function dispose() { 11579 // remove controlTextEl_ on dispose 11580 this.controlTextEl_ = null; 11581 11582 _Component.prototype.dispose.call(this); 11583 }; 11584 11585 /** 11586 * Create a control text element on this `Component` 11587 * 11588 * @param {Element} [el] 11589 * Parent element for the control text. 11590 * 11591 * @return {Element} 11592 * The control text element that gets created. 11593 */ 11594 11595 11596 ClickableComponent.prototype.createControlTextEl = function createControlTextEl(el) { 11597 this.controlTextEl_ = createEl('span', { 11598 className: 'vjs-control-text' 11599 }, { 11600 // let the screen reader user know that the text of the element may change 11601 'aria-live': 'polite' 11602 }); 11603 11604 if (el) { 11605 el.appendChild(this.controlTextEl_); 11606 } 11607 11608 this.controlText(this.controlText_, el); 11609 11610 return this.controlTextEl_; 11611 }; 11612 11613 /** 11614 * Get or set the localize text to use for the controls on the `Component`. 11615 * 11616 * @param {string} [text] 11617 * Control text for element. 11618 * 11619 * @param {Element} [el=this.el()] 11620 * Element to set the title on. 11621 * 11622 * @return {string} 11623 * - The control text when getting 11624 */ 11625 11626 11627 ClickableComponent.prototype.controlText = function controlText(text) { 11628 var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.el(); 11629 11630 if (text === undefined) { 11631 return this.controlText_ || 'Need Text'; 11632 } 11633 11634 var localizedText = this.localize(text); 11635 11636 this.controlText_ = text; 11637 textContent(this.controlTextEl_, localizedText); 11638 if (!this.nonIconControl) { 11639 // Set title attribute if only an icon is shown 11640 el.setAttribute('title', localizedText); 11641 } 11642 }; 11643 11644 /** 11645 * Builds the default DOM `className`. 11646 * 11647 * @return {string} 11648 * The DOM `className` for this object. 11649 */ 11650 11651 11652 ClickableComponent.prototype.buildCSSClass = function buildCSSClass() { 11653 return 'vjs-control vjs-button ' + _Component.prototype.buildCSSClass.call(this); 11654 }; 11655 11656 /** 11657 * Enable this `Component`s element. 11658 */ 11659 11660 11661 ClickableComponent.prototype.enable = function enable() { 11662 if (!this.enabled_) { 11663 this.enabled_ = true; 11664 this.removeClass('vjs-disabled'); 11665 this.el_.setAttribute('aria-disabled', 'false'); 11666 if (typeof this.tabIndex_ !== 'undefined') { 11667 this.el_.setAttribute('tabIndex', this.tabIndex_); 11668 } 11669 this.on(['tap', 'click'], this.handleClick); 11670 this.on('focus', this.handleFocus); 11671 this.on('blur', this.handleBlur); 11672 } 11673 }; 11674 11675 /** 11676 * Disable this `Component`s element. 11677 */ 11678 11679 11680 ClickableComponent.prototype.disable = function disable() { 11681 this.enabled_ = false; 11682 this.addClass('vjs-disabled'); 11683 this.el_.setAttribute('aria-disabled', 'true'); 11684 if (typeof this.tabIndex_ !== 'undefined') { 11685 this.el_.removeAttribute('tabIndex'); 11686 } 11687 this.off(['tap', 'click'], this.handleClick); 11688 this.off('focus', this.handleFocus); 11689 this.off('blur', this.handleBlur); 11690 }; 11691 11692 /** 11693 * This gets called when a `ClickableComponent` gets: 11694 * - Clicked (via the `click` event, listening starts in the constructor) 11695 * - Tapped (via the `tap` event, listening starts in the constructor) 11696 * - The following things happen in order: 11697 * 1. {@link ClickableComponent#handleFocus} is called via a `focus` event on the 11698 * `ClickableComponent`. 11699 * 2. {@link ClickableComponent#handleFocus} adds a listener for `keydown` on using 11700 * {@link ClickableComponent#handleKeyPress}. 11701 * 3. `ClickableComponent` has not had a `blur` event (`blur` means that focus was lost). The user presses 11702 * the space or enter key. 11703 * 4. {@link ClickableComponent#handleKeyPress} calls this function with the `keydown` 11704 * event as a parameter. 11705 * 11706 * @param {EventTarget~Event} event 11707 * The `keydown`, `tap`, or `click` event that caused this function to be 11708 * called. 11709 * 11710 * @listens tap 11711 * @listens click 11712 * @abstract 11713 */ 11714 11715 11716 ClickableComponent.prototype.handleClick = function handleClick(event) {}; 11717 11718 /** 11719 * This gets called when a `ClickableComponent` gains focus via a `focus` event. 11720 * Turns on listening for `keydown` events. When they happen it 11721 * calls `this.handleKeyPress`. 11722 * 11723 * @param {EventTarget~Event} event 11724 * The `focus` event that caused this function to be called. 11725 * 11726 * @listens focus 11727 */ 11728 11729 11730 ClickableComponent.prototype.handleFocus = function handleFocus(event) { 11731 on(document_1, 'keydown', bind(this, this.handleKeyPress)); 11732 }; 11733 11734 /** 11735 * Called when this ClickableComponent has focus and a key gets pressed down. By 11736 * default it will call `this.handleClick` when the key is space or enter. 11737 * 11738 * @param {EventTarget~Event} event 11739 * The `keydown` event that caused this function to be called. 11740 * 11741 * @listens keydown 11742 */ 11743 11744 11745 ClickableComponent.prototype.handleKeyPress = function handleKeyPress(event) { 11746 11747 // Support Space (32) or Enter (13) key operation to fire a click event 11748 if (event.which === 32 || event.which === 13) { 11749 event.preventDefault(); 11750 this.trigger('click'); 11751 } else if (_Component.prototype.handleKeyPress) { 11752 11753 // Pass keypress handling up for unsupported keys 11754 _Component.prototype.handleKeyPress.call(this, event); 11755 } 11756 }; 11757 11758 /** 11759 * Called when a `ClickableComponent` loses focus. Turns off the listener for 11760 * `keydown` events. Which Stops `this.handleKeyPress` from getting called. 11761 * 11762 * @param {EventTarget~Event} event 11763 * The `blur` event that caused this function to be called. 11764 * 11765 * @listens blur 11766 */ 11767 11768 11769 ClickableComponent.prototype.handleBlur = function handleBlur(event) { 11770 off(document_1, 'keydown', bind(this, this.handleKeyPress)); 11771 }; 11772 11773 return ClickableComponent; 11774 }(Component); 11775 11776 Component.registerComponent('ClickableComponent', ClickableComponent); 11777 11778 /** 11779 * @file poster-image.js 11780 */ 11781 11782 /** 11783 * A `ClickableComponent` that handles showing the poster image for the player. 11784 * 11785 * @extends ClickableComponent 11786 */ 11787 11788 var PosterImage = function (_ClickableComponent) { 11789 inherits(PosterImage, _ClickableComponent); 11790 11791 /** 11792 * Create an instance of this class. 11793 * 11794 * @param {Player} player 11795 * The `Player` that this class should attach to. 11796 * 11797 * @param {Object} [options] 11798 * The key/value store of player options. 11799 */ 11800 function PosterImage(player, options) { 11801 classCallCheck(this, PosterImage); 11802 11803 var _this = possibleConstructorReturn(this, _ClickableComponent.call(this, player, options)); 11804 11805 _this.update(); 11806 player.on('posterchange', bind(_this, _this.update)); 11807 return _this; 11808 } 11809 11810 /** 11811 * Clean up and dispose of the `PosterImage`. 11812 */ 11813 11814 11815 PosterImage.prototype.dispose = function dispose() { 11816 this.player().off('posterchange', this.update); 11817 _ClickableComponent.prototype.dispose.call(this); 11818 }; 11819 11820 /** 11821 * Create the `PosterImage`s DOM element. 11822 * 11823 * @return {Element} 11824 * The element that gets created. 11825 */ 11826 11827 11828 PosterImage.prototype.createEl = function createEl$$1() { 11829 var el = createEl('div', { 11830 className: 'vjs-poster', 11831 11832 // Don't want poster to be tabbable. 11833 tabIndex: -1 11834 }); 11835 11836 return el; 11837 }; 11838 11839 /** 11840 * An {@link EventTarget~EventListener} for {@link Player#posterchange} events. 11841 * 11842 * @listens Player#posterchange 11843 * 11844 * @param {EventTarget~Event} [event] 11845 * The `Player#posterchange` event that triggered this function. 11846 */ 11847 11848 11849 PosterImage.prototype.update = function update(event) { 11850 var url = this.player().poster(); 11851 11852 this.setSrc(url); 11853 11854 // If there's no poster source we should display:none on this component 11855 // so it's not still clickable or right-clickable 11856 if (url) { 11857 this.show(); 11858 } else { 11859 this.hide(); 11860 } 11861 }; 11862 11863 /** 11864 * Set the source of the `PosterImage` depending on the display method. 11865 * 11866 * @param {string} url 11867 * The URL to the source for the `PosterImage`. 11868 */ 11869 11870 11871 PosterImage.prototype.setSrc = function setSrc(url) { 11872 var backgroundImage = ''; 11873 11874 // Any falsy value should stay as an empty string, otherwise 11875 // this will throw an extra error 11876 if (url) { 11877 backgroundImage = 'url("' + url + '")'; 11878 } 11879 11880 this.el_.style.backgroundImage = backgroundImage; 11881 }; 11882 11883 /** 11884 * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See 11885 * {@link ClickableComponent#handleClick} for instances where this will be triggered. 11886 * 11887 * @listens tap 11888 * @listens click 11889 * @listens keydown 11890 * 11891 * @param {EventTarget~Event} event 11892 + The `click`, `tap` or `keydown` event that caused this function to be called. 11893 */ 11894 11895 11896 PosterImage.prototype.handleClick = function handleClick(event) { 11897 // We don't want a click to trigger playback when controls are disabled 11898 if (!this.player_.controls()) { 11899 return; 11900 } 11901 11902 if (this.player_.paused()) { 11903 silencePromise(this.player_.play()); 11904 } else { 11905 this.player_.pause(); 11906 } 11907 }; 11908 11909 return PosterImage; 11910 }(ClickableComponent); 11911 11912 Component.registerComponent('PosterImage', PosterImage); 11913 11914 /** 11915 * @file text-track-display.js 11916 */ 11917 11918 var darkGray = '#222'; 11919 var lightGray = '#ccc'; 11920 var fontMap = { 11921 monospace: 'monospace', 11922 sansSerif: 'sans-serif', 11923 serif: 'serif', 11924 monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', 11925 monospaceSerif: '"Courier New", monospace', 11926 proportionalSansSerif: 'sans-serif', 11927 proportionalSerif: 'serif', 11928 casual: '"Comic Sans MS", Impact, fantasy', 11929 script: '"Monotype Corsiva", cursive', 11930 smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' 11931 }; 11932 11933 /** 11934 * Construct an rgba color from a given hex color code. 11935 * 11936 * @param {number} color 11937 * Hex number for color, like #f0e. 11938 * 11939 * @param {number} opacity 11940 * Value for opacity, 0.0 - 1.0. 11941 * 11942 * @return {string} 11943 * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'. 11944 * 11945 * @private 11946 */ 11947 function constructColor(color, opacity) { 11948 return 'rgba(' + 11949 // color looks like "#f0e" 11950 parseInt(color[1] + color[1], 16) + ',' + parseInt(color[2] + color[2], 16) + ',' + parseInt(color[3] + color[3], 16) + ',' + opacity + ')'; 11951 } 11952 11953 /** 11954 * Try to update the style of a DOM element. Some style changes will throw an error, 11955 * particularly in IE8. Those should be noops. 11956 * 11957 * @param {Element} el 11958 * The DOM element to be styled. 11959 * 11960 * @param {string} style 11961 * The CSS property on the element that should be styled. 11962 * 11963 * @param {string} rule 11964 * The style rule that should be applied to the property. 11965 * 11966 * @private 11967 */ 11968 function tryUpdateStyle(el, style, rule) { 11969 try { 11970 el.style[style] = rule; 11971 } catch (e) { 11972 11973 // Satisfies linter. 11974 return; 11975 } 11976 } 11977 11978 /** 11979 * The component for displaying text track cues. 11980 * 11981 * @extends Component 11982 */ 11983 11984 var TextTrackDisplay = function (_Component) { 11985 inherits(TextTrackDisplay, _Component); 11986 11987 /** 11988 * Creates an instance of this class. 11989 * 11990 * @param {Player} player 11991 * The `Player` that this class should be attached to. 11992 * 11993 * @param {Object} [options] 11994 * The key/value store of player options. 11995 * 11996 * @param {Component~ReadyCallback} [ready] 11997 * The function to call when `TextTrackDisplay` is ready. 11998 */ 11999 function TextTrackDisplay(player, options, ready) { 12000 classCallCheck(this, TextTrackDisplay); 12001 12002 var _this = possibleConstructorReturn(this, _Component.call(this, player, options, ready)); 12003 12004 player.on('loadstart', bind(_this, _this.toggleDisplay)); 12005 player.on('texttrackchange', bind(_this, _this.updateDisplay)); 12006 player.on('loadstart', bind(_this, _this.preselectTrack)); 12007 12008 // This used to be called during player init, but was causing an error 12009 // if a track should show by default and the display hadn't loaded yet. 12010 // Should probably be moved to an external track loader when we support 12011 // tracks that don't need a display. 12012 player.ready(bind(_this, function () { 12013 if (player.tech_ && player.tech_.featuresNativeTextTracks) { 12014 this.hide(); 12015 return; 12016 } 12017 12018 player.on('fullscreenchange', bind(this, this.updateDisplay)); 12019 12020 var tracks = this.options_.playerOptions.tracks || []; 12021 12022 for (var i = 0; i < tracks.length; i++) { 12023 this.player_.addRemoteTextTrack(tracks[i], true); 12024 } 12025 12026 this.preselectTrack(); 12027 })); 12028 return _this; 12029 } 12030 12031 /** 12032 * Preselect a track following this precedence: 12033 * - matches the previously selected {@link TextTrack}'s language and kind 12034 * - matches the previously selected {@link TextTrack}'s language only 12035 * - is the first default captions track 12036 * - is the first default descriptions track 12037 * 12038 * @listens Player#loadstart 12039 */ 12040 12041 12042 TextTrackDisplay.prototype.preselectTrack = function preselectTrack() { 12043 var modes = { captions: 1, subtitles: 1 }; 12044 var trackList = this.player_.textTracks(); 12045 var userPref = this.player_.cache_.selectedLanguage; 12046 var firstDesc = void 0; 12047 var firstCaptions = void 0; 12048 var preferredTrack = void 0; 12049 12050 for (var i = 0; i < trackList.length; i++) { 12051 var track = trackList[i]; 12052 12053 if (userPref && userPref.enabled && userPref.language === track.language) { 12054 // Always choose the track that matches both language and kind 12055 if (track.kind === userPref.kind) { 12056 preferredTrack = track; 12057 // or choose the first track that matches language 12058 } else if (!preferredTrack) { 12059 preferredTrack = track; 12060 } 12061 12062 // clear everything if offTextTrackMenuItem was clicked 12063 } else if (userPref && !userPref.enabled) { 12064 preferredTrack = null; 12065 firstDesc = null; 12066 firstCaptions = null; 12067 } else if (track.default) { 12068 if (track.kind === 'descriptions' && !firstDesc) { 12069 firstDesc = track; 12070 } else if (track.kind in modes && !firstCaptions) { 12071 firstCaptions = track; 12072 } 12073 } 12074 } 12075 12076 // The preferredTrack matches the user preference and takes 12077 // precedence over all the other tracks. 12078 // So, display the preferredTrack before the first default track 12079 // and the subtitles/captions track before the descriptions track 12080 if (preferredTrack) { 12081 preferredTrack.mode = 'showing'; 12082 } else if (firstCaptions) { 12083 firstCaptions.mode = 'showing'; 12084 } else if (firstDesc) { 12085 firstDesc.mode = 'showing'; 12086 } 12087 }; 12088 12089 /** 12090 * Turn display of {@link TextTrack}'s from the current state into the other state. 12091 * There are only two states: 12092 * - 'shown' 12093 * - 'hidden' 12094 * 12095 * @listens Player#loadstart 12096 */ 12097 12098 12099 TextTrackDisplay.prototype.toggleDisplay = function toggleDisplay() { 12100 if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) { 12101 this.hide(); 12102 } else { 12103 this.show(); 12104 } 12105 }; 12106 12107 /** 12108 * Create the {@link Component}'s DOM element. 12109 * 12110 * @return {Element} 12111 * The element that was created. 12112 */ 12113 12114 12115 TextTrackDisplay.prototype.createEl = function createEl() { 12116 return _Component.prototype.createEl.call(this, 'div', { 12117 className: 'vjs-text-track-display' 12118 }, { 12119 'aria-live': 'off', 12120 'aria-atomic': 'true' 12121 }); 12122 }; 12123 12124 /** 12125 * Clear all displayed {@link TextTrack}s. 12126 */ 12127 12128 12129 TextTrackDisplay.prototype.clearDisplay = function clearDisplay() { 12130 if (typeof window_1.WebVTT === 'function') { 12131 window_1.WebVTT.processCues(window_1, [], this.el_); 12132 } 12133 }; 12134 12135 /** 12136 * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or 12137 * a {@link Player#fullscreenchange} is fired. 12138 * 12139 * @listens Player#texttrackchange 12140 * @listens Player#fullscreenchange 12141 */ 12142 12143 12144 TextTrackDisplay.prototype.updateDisplay = function updateDisplay() { 12145 var tracks = this.player_.textTracks(); 12146 12147 this.clearDisplay(); 12148 12149 // Track display prioritization model: if multiple tracks are 'showing', 12150 // display the first 'subtitles' or 'captions' track which is 'showing', 12151 // otherwise display the first 'descriptions' track which is 'showing' 12152 12153 var descriptionsTrack = null; 12154 var captionsSubtitlesTrack = null; 12155 var i = tracks.length; 12156 12157 while (i--) { 12158 var track = tracks[i]; 12159 12160 if (track.mode === 'showing') { 12161 if (track.kind === 'descriptions') { 12162 descriptionsTrack = track; 12163 } else { 12164 captionsSubtitlesTrack = track; 12165 } 12166 } 12167 } 12168 12169 if (captionsSubtitlesTrack) { 12170 if (this.getAttribute('aria-live') !== 'off') { 12171 this.setAttribute('aria-live', 'off'); 12172 } 12173 this.updateForTrack(captionsSubtitlesTrack); 12174 } else if (descriptionsTrack) { 12175 if (this.getAttribute('aria-live') !== 'assertive') { 12176 this.setAttribute('aria-live', 'assertive'); 12177 } 12178 this.updateForTrack(descriptionsTrack); 12179 } 12180 }; 12181 12182 /** 12183 * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}. 12184 * 12185 * @param {TextTrack} track 12186 * Text track object to be added to the list. 12187 */ 12188 12189 12190 TextTrackDisplay.prototype.updateForTrack = function updateForTrack(track) { 12191 if (typeof window_1.WebVTT !== 'function' || !track.activeCues) { 12192 return; 12193 } 12194 12195 var cues = []; 12196 12197 for (var _i = 0; _i < track.activeCues.length; _i++) { 12198 cues.push(track.activeCues[_i]); 12199 } 12200 12201 window_1.WebVTT.processCues(window_1, cues, this.el_); 12202 12203 if (!this.player_.textTrackSettings) { 12204 return; 12205 } 12206 12207 var overrides = this.player_.textTrackSettings.getValues(); 12208 12209 var i = cues.length; 12210 12211 while (i--) { 12212 var cue = cues[i]; 12213 12214 if (!cue) { 12215 continue; 12216 } 12217 12218 var cueDiv = cue.displayState; 12219 12220 if (overrides.color) { 12221 cueDiv.firstChild.style.color = overrides.color; 12222 } 12223 if (overrides.textOpacity) { 12224 tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity)); 12225 } 12226 if (overrides.backgroundColor) { 12227 cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; 12228 } 12229 if (overrides.backgroundOpacity) { 12230 tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity)); 12231 } 12232 if (overrides.windowColor) { 12233 if (overrides.windowOpacity) { 12234 tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity)); 12235 } else { 12236 cueDiv.style.backgroundColor = overrides.windowColor; 12237 } 12238 } 12239 if (overrides.edgeStyle) { 12240 if (overrides.edgeStyle === 'dropshadow') { 12241 cueDiv.firstChild.style.textShadow = '2px 2px 3px ' + darkGray + ', 2px 2px 4px ' + darkGray + ', 2px 2px 5px ' + darkGray; 12242 } else if (overrides.edgeStyle === 'raised') { 12243 cueDiv.firstChild.style.textShadow = '1px 1px ' + darkGray + ', 2px 2px ' + darkGray + ', 3px 3px ' + darkGray; 12244 } else if (overrides.edgeStyle === 'depressed') { 12245 cueDiv.firstChild.style.textShadow = '1px 1px ' + lightGray + ', 0 1px ' + lightGray + ', -1px -1px ' + darkGray + ', 0 -1px ' + darkGray; 12246 } else if (overrides.edgeStyle === 'uniform') { 12247 cueDiv.firstChild.style.textShadow = '0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray; 12248 } 12249 } 12250 if (overrides.fontPercent && overrides.fontPercent !== 1) { 12251 var fontSize = window_1.parseFloat(cueDiv.style.fontSize); 12252 12253 cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px'; 12254 cueDiv.style.height = 'auto'; 12255 cueDiv.style.top = 'auto'; 12256 cueDiv.style.bottom = '2px'; 12257 } 12258 if (overrides.fontFamily && overrides.fontFamily !== 'default') { 12259 if (overrides.fontFamily === 'small-caps') { 12260 cueDiv.firstChild.style.fontVariant = 'small-caps'; 12261 } else { 12262 cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; 12263 } 12264 } 12265 } 12266 }; 12267 12268 return TextTrackDisplay; 12269 }(Component); 12270 12271 Component.registerComponent('TextTrackDisplay', TextTrackDisplay); 12272 12273 /** 12274 * @file loading-spinner.js 12275 */ 12276 12277 /** 12278 * A loading spinner for use during waiting/loading events. 12279 * 12280 * @extends Component 12281 */ 12282 12283 var LoadingSpinner = function (_Component) { 12284 inherits(LoadingSpinner, _Component); 12285 12286 function LoadingSpinner() { 12287 classCallCheck(this, LoadingSpinner); 12288 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 12289 } 12290 12291 /** 12292 * Create the `LoadingSpinner`s DOM element. 12293 * 12294 * @return {Element} 12295 * The dom element that gets created. 12296 */ 12297 LoadingSpinner.prototype.createEl = function createEl$$1() { 12298 var isAudio = this.player_.isAudio(); 12299 var playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player'); 12300 var controlText = createEl('span', { 12301 className: 'vjs-control-text', 12302 innerHTML: this.localize('{1} is loading.', [playerType]) 12303 }); 12304 12305 var el = _Component.prototype.createEl.call(this, 'div', { 12306 className: 'vjs-loading-spinner', 12307 dir: 'ltr' 12308 }); 12309 12310 el.appendChild(controlText); 12311 12312 return el; 12313 }; 12314 12315 return LoadingSpinner; 12316 }(Component); 12317 12318 Component.registerComponent('LoadingSpinner', LoadingSpinner); 12319 12320 /** 12321 * @file button.js 12322 */ 12323 12324 /** 12325 * Base class for all buttons. 12326 * 12327 * @extends ClickableComponent 12328 */ 12329 12330 var Button = function (_ClickableComponent) { 12331 inherits(Button, _ClickableComponent); 12332 12333 function Button() { 12334 classCallCheck(this, Button); 12335 return possibleConstructorReturn(this, _ClickableComponent.apply(this, arguments)); 12336 } 12337 12338 /** 12339 * Create the `Button`s DOM element. 12340 * 12341 * @param {string} [tag="button"] 12342 * The element's node type. This argument is IGNORED: no matter what 12343 * is passed, it will always create a `button` element. 12344 * 12345 * @param {Object} [props={}] 12346 * An object of properties that should be set on the element. 12347 * 12348 * @param {Object} [attributes={}] 12349 * An object of attributes that should be set on the element. 12350 * 12351 * @return {Element} 12352 * The element that gets created. 12353 */ 12354 Button.prototype.createEl = function createEl(tag) { 12355 var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 12356 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 12357 12358 tag = 'button'; 12359 12360 props = assign({ 12361 innerHTML: '<span aria-hidden="true" class="vjs-icon-placeholder"></span>', 12362 className: this.buildCSSClass() 12363 }, props); 12364 12365 // Add attributes for button element 12366 attributes = assign({ 12367 12368 // Necessary since the default button type is "submit" 12369 type: 'button' 12370 }, attributes); 12371 12372 var el = Component.prototype.createEl.call(this, tag, props, attributes); 12373 12374 this.createControlTextEl(el); 12375 12376 return el; 12377 }; 12378 12379 /** 12380 * Add a child `Component` inside of this `Button`. 12381 * 12382 * @param {string|Component} child 12383 * The name or instance of a child to add. 12384 * 12385 * @param {Object} [options={}] 12386 * The key/value store of options that will get passed to children of 12387 * the child. 12388 * 12389 * @return {Component} 12390 * The `Component` that gets added as a child. When using a string the 12391 * `Component` will get created by this process. 12392 * 12393 * @deprecated since version 5 12394 */ 12395 12396 12397 Button.prototype.addChild = function addChild(child) { 12398 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 12399 12400 var className = this.constructor.name; 12401 12402 log$1.warn('Adding an actionable (user controllable) child to a Button (' + className + ') is not supported; use a ClickableComponent instead.'); 12403 12404 // Avoid the error message generated by ClickableComponent's addChild method 12405 return Component.prototype.addChild.call(this, child, options); 12406 }; 12407 12408 /** 12409 * Enable the `Button` element so that it can be activated or clicked. Use this with 12410 * {@link Button#disable}. 12411 */ 12412 12413 12414 Button.prototype.enable = function enable() { 12415 _ClickableComponent.prototype.enable.call(this); 12416 this.el_.removeAttribute('disabled'); 12417 }; 12418 12419 /** 12420 * Disable the `Button` element so that it cannot be activated or clicked. Use this with 12421 * {@link Button#enable}. 12422 */ 12423 12424 12425 Button.prototype.disable = function disable() { 12426 _ClickableComponent.prototype.disable.call(this); 12427 this.el_.setAttribute('disabled', 'disabled'); 12428 }; 12429 12430 /** 12431 * This gets called when a `Button` has focus and `keydown` is triggered via a key 12432 * press. 12433 * 12434 * @param {EventTarget~Event} event 12435 * The event that caused this function to get called. 12436 * 12437 * @listens keydown 12438 */ 12439 12440 12441 Button.prototype.handleKeyPress = function handleKeyPress(event) { 12442 12443 // Ignore Space (32) or Enter (13) key operation, which is handled by the browser for a button. 12444 if (event.which === 32 || event.which === 13) { 12445 return; 12446 } 12447 12448 // Pass keypress handling up for unsupported keys 12449 _ClickableComponent.prototype.handleKeyPress.call(this, event); 12450 }; 12451 12452 return Button; 12453 }(ClickableComponent); 12454 12455 Component.registerComponent('Button', Button); 12456 12457 /** 12458 * @file big-play-button.js 12459 */ 12460 12461 /** 12462 * The initial play button that shows before the video has played. The hiding of the 12463 * `BigPlayButton` get done via CSS and `Player` states. 12464 * 12465 * @extends Button 12466 */ 12467 12468 var BigPlayButton = function (_Button) { 12469 inherits(BigPlayButton, _Button); 12470 12471 function BigPlayButton(player, options) { 12472 classCallCheck(this, BigPlayButton); 12473 12474 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 12475 12476 _this.mouseused_ = false; 12477 12478 _this.on('mousedown', _this.handleMouseDown); 12479 return _this; 12480 } 12481 12482 /** 12483 * Builds the default DOM `className`. 12484 * 12485 * @return {string} 12486 * The DOM `className` for this object. Always returns 'vjs-big-play-button'. 12487 */ 12488 12489 12490 BigPlayButton.prototype.buildCSSClass = function buildCSSClass() { 12491 return 'vjs-big-play-button'; 12492 }; 12493 12494 /** 12495 * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent} 12496 * for more detailed information on what a click can be. 12497 * 12498 * @param {EventTarget~Event} event 12499 * The `keydown`, `tap`, or `click` event that caused this function to be 12500 * called. 12501 * 12502 * @listens tap 12503 * @listens click 12504 */ 12505 12506 12507 BigPlayButton.prototype.handleClick = function handleClick(event) { 12508 var playPromise = this.player_.play(); 12509 12510 // exit early if clicked via the mouse 12511 if (this.mouseused_ && event.clientX && event.clientY) { 12512 silencePromise(playPromise); 12513 return; 12514 } 12515 12516 var cb = this.player_.getChild('controlBar'); 12517 var playToggle = cb && cb.getChild('playToggle'); 12518 12519 if (!playToggle) { 12520 this.player_.focus(); 12521 return; 12522 } 12523 12524 var playFocus = function playFocus() { 12525 return playToggle.focus(); 12526 }; 12527 12528 if (isPromise(playPromise)) { 12529 playPromise.then(playFocus, function () {}); 12530 } else { 12531 this.setTimeout(playFocus, 1); 12532 } 12533 }; 12534 12535 BigPlayButton.prototype.handleKeyPress = function handleKeyPress(event) { 12536 this.mouseused_ = false; 12537 12538 _Button.prototype.handleKeyPress.call(this, event); 12539 }; 12540 12541 BigPlayButton.prototype.handleMouseDown = function handleMouseDown(event) { 12542 this.mouseused_ = true; 12543 }; 12544 12545 return BigPlayButton; 12546 }(Button); 12547 12548 /** 12549 * The text that should display over the `BigPlayButton`s controls. Added to for localization. 12550 * 12551 * @type {string} 12552 * @private 12553 */ 12554 12555 12556 BigPlayButton.prototype.controlText_ = 'Play Video'; 12557 12558 Component.registerComponent('BigPlayButton', BigPlayButton); 12559 12560 /** 12561 * @file close-button.js 12562 */ 12563 12564 /** 12565 * The `CloseButton` is a `{@link Button}` that fires a `close` event when 12566 * it gets clicked. 12567 * 12568 * @extends Button 12569 */ 12570 12571 var CloseButton = function (_Button) { 12572 inherits(CloseButton, _Button); 12573 12574 /** 12575 * Creates an instance of the this class. 12576 * 12577 * @param {Player} player 12578 * The `Player` that this class should be attached to. 12579 * 12580 * @param {Object} [options] 12581 * The key/value store of player options. 12582 */ 12583 function CloseButton(player, options) { 12584 classCallCheck(this, CloseButton); 12585 12586 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 12587 12588 _this.controlText(options && options.controlText || _this.localize('Close')); 12589 return _this; 12590 } 12591 12592 /** 12593 * Builds the default DOM `className`. 12594 * 12595 * @return {string} 12596 * The DOM `className` for this object. 12597 */ 12598 12599 12600 CloseButton.prototype.buildCSSClass = function buildCSSClass() { 12601 return 'vjs-close-button ' + _Button.prototype.buildCSSClass.call(this); 12602 }; 12603 12604 /** 12605 * This gets called when a `CloseButton` gets clicked. See 12606 * {@link ClickableComponent#handleClick} for more information on when this will be 12607 * triggered 12608 * 12609 * @param {EventTarget~Event} event 12610 * The `keydown`, `tap`, or `click` event that caused this function to be 12611 * called. 12612 * 12613 * @listens tap 12614 * @listens click 12615 * @fires CloseButton#close 12616 */ 12617 12618 12619 CloseButton.prototype.handleClick = function handleClick(event) { 12620 12621 /** 12622 * Triggered when the a `CloseButton` is clicked. 12623 * 12624 * @event CloseButton#close 12625 * @type {EventTarget~Event} 12626 * 12627 * @property {boolean} [bubbles=false] 12628 * set to false so that the close event does not 12629 * bubble up to parents if there is no listener 12630 */ 12631 this.trigger({ type: 'close', bubbles: false }); 12632 }; 12633 12634 return CloseButton; 12635 }(Button); 12636 12637 Component.registerComponent('CloseButton', CloseButton); 12638 12639 /** 12640 * @file play-toggle.js 12641 */ 12642 12643 /** 12644 * Button to toggle between play and pause. 12645 * 12646 * @extends Button 12647 */ 12648 12649 var PlayToggle = function (_Button) { 12650 inherits(PlayToggle, _Button); 12651 12652 /** 12653 * Creates an instance of this class. 12654 * 12655 * @param {Player} player 12656 * The `Player` that this class should be attached to. 12657 * 12658 * @param {Object} [options] 12659 * The key/value store of player options. 12660 */ 12661 function PlayToggle(player, options) { 12662 classCallCheck(this, PlayToggle); 12663 12664 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 12665 12666 _this.on(player, 'play', _this.handlePlay); 12667 _this.on(player, 'pause', _this.handlePause); 12668 _this.on(player, 'ended', _this.handleEnded); 12669 return _this; 12670 } 12671 12672 /** 12673 * Builds the default DOM `className`. 12674 * 12675 * @return {string} 12676 * The DOM `className` for this object. 12677 */ 12678 12679 12680 PlayToggle.prototype.buildCSSClass = function buildCSSClass() { 12681 return 'vjs-play-control ' + _Button.prototype.buildCSSClass.call(this); 12682 }; 12683 12684 /** 12685 * This gets called when an `PlayToggle` is "clicked". See 12686 * {@link ClickableComponent} for more detailed information on what a click can be. 12687 * 12688 * @param {EventTarget~Event} [event] 12689 * The `keydown`, `tap`, or `click` event that caused this function to be 12690 * called. 12691 * 12692 * @listens tap 12693 * @listens click 12694 */ 12695 12696 12697 PlayToggle.prototype.handleClick = function handleClick(event) { 12698 if (this.player_.paused()) { 12699 this.player_.play(); 12700 } else { 12701 this.player_.pause(); 12702 } 12703 }; 12704 12705 /** 12706 * This gets called once after the video has ended and the user seeks so that 12707 * we can change the replay button back to a play button. 12708 * 12709 * @param {EventTarget~Event} [event] 12710 * The event that caused this function to run. 12711 * 12712 * @listens Player#seeked 12713 */ 12714 12715 12716 PlayToggle.prototype.handleSeeked = function handleSeeked(event) { 12717 this.removeClass('vjs-ended'); 12718 12719 if (this.player_.paused()) { 12720 this.handlePause(event); 12721 } else { 12722 this.handlePlay(event); 12723 } 12724 }; 12725 12726 /** 12727 * Add the vjs-playing class to the element so it can change appearance. 12728 * 12729 * @param {EventTarget~Event} [event] 12730 * The event that caused this function to run. 12731 * 12732 * @listens Player#play 12733 */ 12734 12735 12736 PlayToggle.prototype.handlePlay = function handlePlay(event) { 12737 this.removeClass('vjs-ended'); 12738 this.removeClass('vjs-paused'); 12739 this.addClass('vjs-playing'); 12740 // change the button text to "Pause" 12741 this.controlText('Pause'); 12742 }; 12743 12744 /** 12745 * Add the vjs-paused class to the element so it can change appearance. 12746 * 12747 * @param {EventTarget~Event} [event] 12748 * The event that caused this function to run. 12749 * 12750 * @listens Player#pause 12751 */ 12752 12753 12754 PlayToggle.prototype.handlePause = function handlePause(event) { 12755 this.removeClass('vjs-playing'); 12756 this.addClass('vjs-paused'); 12757 // change the button text to "Play" 12758 this.controlText('Play'); 12759 }; 12760 12761 /** 12762 * Add the vjs-ended class to the element so it can change appearance 12763 * 12764 * @param {EventTarget~Event} [event] 12765 * The event that caused this function to run. 12766 * 12767 * @listens Player#ended 12768 */ 12769 12770 12771 PlayToggle.prototype.handleEnded = function handleEnded(event) { 12772 this.removeClass('vjs-playing'); 12773 this.addClass('vjs-ended'); 12774 // change the button text to "Replay" 12775 this.controlText('Replay'); 12776 12777 // on the next seek remove the replay button 12778 this.one(this.player_, 'seeked', this.handleSeeked); 12779 }; 12780 12781 return PlayToggle; 12782 }(Button); 12783 12784 /** 12785 * The text that should display over the `PlayToggle`s controls. Added for localization. 12786 * 12787 * @type {string} 12788 * @private 12789 */ 12790 12791 12792 PlayToggle.prototype.controlText_ = 'Play'; 12793 12794 Component.registerComponent('PlayToggle', PlayToggle); 12795 12796 /** 12797 * @file format-time.js 12798 * @module format-time 12799 */ 12800 12801 /** 12802 * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in seconds) 12803 * will force a number of leading zeros to cover the length of the guide. 12804 * 12805 * @param {number} seconds 12806 * Number of seconds to be turned into a string 12807 * 12808 * @param {number} guide 12809 * Number (in seconds) to model the string after 12810 * 12811 * @return {string} 12812 * Time formatted as H:MM:SS or M:SS 12813 */ 12814 var defaultImplementation = function defaultImplementation(seconds, guide) { 12815 seconds = seconds < 0 ? 0 : seconds; 12816 var s = Math.floor(seconds % 60); 12817 var m = Math.floor(seconds / 60 % 60); 12818 var h = Math.floor(seconds / 3600); 12819 var gm = Math.floor(guide / 60 % 60); 12820 var gh = Math.floor(guide / 3600); 12821 12822 // handle invalid times 12823 if (isNaN(seconds) || seconds === Infinity) { 12824 // '-' is false for all relational operators (e.g. <, >=) so this setting 12825 // will add the minimum number of fields specified by the guide 12826 h = m = s = '-'; 12827 } 12828 12829 // Check if we need to show hours 12830 h = h > 0 || gh > 0 ? h + ':' : ''; 12831 12832 // If hours are showing, we may need to add a leading zero. 12833 // Always show at least one digit of minutes. 12834 m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':'; 12835 12836 // Check if leading zero is need for seconds 12837 s = s < 10 ? '0' + s : s; 12838 12839 return h + m + s; 12840 }; 12841 12842 var implementation = defaultImplementation; 12843 12844 /** 12845 * Replaces the default formatTime implementation with a custom implementation. 12846 * 12847 * @param {Function} customImplementation 12848 * A function which will be used in place of the default formatTime implementation. 12849 * Will receive the current time in seconds and the guide (in seconds) as arguments. 12850 */ 12851 function setFormatTime(customImplementation) { 12852 implementation = customImplementation; 12853 } 12854 12855 /** 12856 * Resets formatTime to the default implementation. 12857 */ 12858 function resetFormatTime() { 12859 implementation = defaultImplementation; 12860 } 12861 12862 function formatTime (seconds) { 12863 var guide = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : seconds; 12864 12865 return implementation(seconds, guide); 12866 } 12867 12868 /** 12869 * @file time-display.js 12870 */ 12871 12872 /** 12873 * Displays the time left in the video 12874 * 12875 * @extends Component 12876 */ 12877 12878 var TimeDisplay = function (_Component) { 12879 inherits(TimeDisplay, _Component); 12880 12881 /** 12882 * Creates an instance of this class. 12883 * 12884 * @param {Player} player 12885 * The `Player` that this class should be attached to. 12886 * 12887 * @param {Object} [options] 12888 * The key/value store of player options. 12889 */ 12890 function TimeDisplay(player, options) { 12891 classCallCheck(this, TimeDisplay); 12892 12893 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 12894 12895 _this.throttledUpdateContent = throttle(bind(_this, _this.updateContent), 25); 12896 _this.on(player, 'timeupdate', _this.throttledUpdateContent); 12897 return _this; 12898 } 12899 12900 /** 12901 * Create the `Component`'s DOM element 12902 * 12903 * @return {Element} 12904 * The element that was created. 12905 */ 12906 12907 12908 TimeDisplay.prototype.createEl = function createEl$$1(plainName) { 12909 var className = this.buildCSSClass(); 12910 var el = _Component.prototype.createEl.call(this, 'div', { 12911 className: className + ' vjs-time-control vjs-control', 12912 innerHTML: '<span class="vjs-control-text">' + this.localize(this.labelText_) + '\xA0</span>' 12913 }); 12914 12915 this.contentEl_ = createEl('span', { 12916 className: className + '-display' 12917 }, { 12918 // tell screen readers not to automatically read the time as it changes 12919 'aria-live': 'off' 12920 }); 12921 12922 this.updateTextNode_(); 12923 el.appendChild(this.contentEl_); 12924 return el; 12925 }; 12926 12927 TimeDisplay.prototype.dispose = function dispose() { 12928 this.contentEl_ = null; 12929 this.textNode_ = null; 12930 12931 _Component.prototype.dispose.call(this); 12932 }; 12933 12934 /** 12935 * Updates the "remaining time" text node with new content using the 12936 * contents of the `formattedTime_` property. 12937 * 12938 * @private 12939 */ 12940 12941 12942 TimeDisplay.prototype.updateTextNode_ = function updateTextNode_() { 12943 if (!this.contentEl_) { 12944 return; 12945 } 12946 12947 while (this.contentEl_.firstChild) { 12948 this.contentEl_.removeChild(this.contentEl_.firstChild); 12949 } 12950 12951 this.textNode_ = document_1.createTextNode(this.formattedTime_ || this.formatTime_(0)); 12952 this.contentEl_.appendChild(this.textNode_); 12953 }; 12954 12955 /** 12956 * Generates a formatted time for this component to use in display. 12957 * 12958 * @param {number} time 12959 * A numeric time, in seconds. 12960 * 12961 * @return {string} 12962 * A formatted time 12963 * 12964 * @private 12965 */ 12966 12967 12968 TimeDisplay.prototype.formatTime_ = function formatTime_(time) { 12969 return formatTime(time); 12970 }; 12971 12972 /** 12973 * Updates the time display text node if it has what was passed in changed 12974 * the formatted time. 12975 * 12976 * @param {number} time 12977 * The time to update to 12978 * 12979 * @private 12980 */ 12981 12982 12983 TimeDisplay.prototype.updateFormattedTime_ = function updateFormattedTime_(time) { 12984 var formattedTime = this.formatTime_(time); 12985 12986 if (formattedTime === this.formattedTime_) { 12987 return; 12988 } 12989 12990 this.formattedTime_ = formattedTime; 12991 this.requestAnimationFrame(this.updateTextNode_); 12992 }; 12993 12994 /** 12995 * To be filled out in the child class, should update the displayed time 12996 * in accordance with the fact that the current time has changed. 12997 * 12998 * @param {EventTarget~Event} [event] 12999 * The `timeupdate` event that caused this to run. 13000 * 13001 * @listens Player#timeupdate 13002 */ 13003 13004 13005 TimeDisplay.prototype.updateContent = function updateContent(event) {}; 13006 13007 return TimeDisplay; 13008 }(Component); 13009 13010 /** 13011 * The text that is added to the `TimeDisplay` for screen reader users. 13012 * 13013 * @type {string} 13014 * @private 13015 */ 13016 13017 13018 TimeDisplay.prototype.labelText_ = 'Time'; 13019 13020 /** 13021 * The text that should display over the `TimeDisplay`s controls. Added to for localization. 13022 * 13023 * @type {string} 13024 * @private 13025 * 13026 * @deprecated in v7; controlText_ is not used in non-active display Components 13027 */ 13028 TimeDisplay.prototype.controlText_ = 'Time'; 13029 13030 Component.registerComponent('TimeDisplay', TimeDisplay); 13031 13032 /** 13033 * @file current-time-display.js 13034 */ 13035 13036 /** 13037 * Displays the current time 13038 * 13039 * @extends Component 13040 */ 13041 13042 var CurrentTimeDisplay = function (_TimeDisplay) { 13043 inherits(CurrentTimeDisplay, _TimeDisplay); 13044 13045 /** 13046 * Creates an instance of this class. 13047 * 13048 * @param {Player} player 13049 * The `Player` that this class should be attached to. 13050 * 13051 * @param {Object} [options] 13052 * The key/value store of player options. 13053 */ 13054 function CurrentTimeDisplay(player, options) { 13055 classCallCheck(this, CurrentTimeDisplay); 13056 13057 var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); 13058 13059 _this.on(player, 'ended', _this.handleEnded); 13060 return _this; 13061 } 13062 13063 /** 13064 * Builds the default DOM `className`. 13065 * 13066 * @return {string} 13067 * The DOM `className` for this object. 13068 */ 13069 13070 13071 CurrentTimeDisplay.prototype.buildCSSClass = function buildCSSClass() { 13072 return 'vjs-current-time'; 13073 }; 13074 13075 /** 13076 * Update current time display 13077 * 13078 * @param {EventTarget~Event} [event] 13079 * The `timeupdate` event that caused this function to run. 13080 * 13081 * @listens Player#timeupdate 13082 */ 13083 13084 13085 CurrentTimeDisplay.prototype.updateContent = function updateContent(event) { 13086 // Allows for smooth scrubbing, when player can't keep up. 13087 var time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 13088 13089 this.updateFormattedTime_(time); 13090 }; 13091 13092 /** 13093 * When the player fires ended there should be no time left. Sadly 13094 * this is not always the case, lets make it seem like that is the case 13095 * for users. 13096 * 13097 * @param {EventTarget~Event} [event] 13098 * The `ended` event that caused this to run. 13099 * 13100 * @listens Player#ended 13101 */ 13102 13103 13104 CurrentTimeDisplay.prototype.handleEnded = function handleEnded(event) { 13105 if (!this.player_.duration()) { 13106 return; 13107 } 13108 this.updateFormattedTime_(this.player_.duration()); 13109 }; 13110 13111 return CurrentTimeDisplay; 13112 }(TimeDisplay); 13113 13114 /** 13115 * The text that is added to the `CurrentTimeDisplay` for screen reader users. 13116 * 13117 * @type {string} 13118 * @private 13119 */ 13120 13121 13122 CurrentTimeDisplay.prototype.labelText_ = 'Current Time'; 13123 13124 /** 13125 * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization. 13126 * 13127 * @type {string} 13128 * @private 13129 * 13130 * @deprecated in v7; controlText_ is not used in non-active display Components 13131 */ 13132 CurrentTimeDisplay.prototype.controlText_ = 'Current Time'; 13133 13134 Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); 13135 13136 /** 13137 * @file duration-display.js 13138 */ 13139 13140 /** 13141 * Displays the duration 13142 * 13143 * @extends Component 13144 */ 13145 13146 var DurationDisplay = function (_TimeDisplay) { 13147 inherits(DurationDisplay, _TimeDisplay); 13148 13149 /** 13150 * Creates an instance of this class. 13151 * 13152 * @param {Player} player 13153 * The `Player` that this class should be attached to. 13154 * 13155 * @param {Object} [options] 13156 * The key/value store of player options. 13157 */ 13158 function DurationDisplay(player, options) { 13159 classCallCheck(this, DurationDisplay); 13160 13161 // we do not want to/need to throttle duration changes, 13162 // as they should always display the changed duration as 13163 // it has changed 13164 var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); 13165 13166 _this.on(player, 'durationchange', _this.updateContent); 13167 13168 // Also listen for timeupdate (in the parent) and loadedmetadata because removing those 13169 // listeners could have broken dependent applications/libraries. These 13170 // can likely be removed for 7.0. 13171 _this.on(player, 'loadedmetadata', _this.throttledUpdateContent); 13172 return _this; 13173 } 13174 13175 /** 13176 * Builds the default DOM `className`. 13177 * 13178 * @return {string} 13179 * The DOM `className` for this object. 13180 */ 13181 13182 13183 DurationDisplay.prototype.buildCSSClass = function buildCSSClass() { 13184 return 'vjs-duration'; 13185 }; 13186 13187 /** 13188 * Update duration time display. 13189 * 13190 * @param {EventTarget~Event} [event] 13191 * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused 13192 * this function to be called. 13193 * 13194 * @listens Player#durationchange 13195 * @listens Player#timeupdate 13196 * @listens Player#loadedmetadata 13197 */ 13198 13199 13200 DurationDisplay.prototype.updateContent = function updateContent(event) { 13201 var duration = this.player_.duration(); 13202 13203 if (duration && this.duration_ !== duration) { 13204 this.duration_ = duration; 13205 this.updateFormattedTime_(duration); 13206 } 13207 }; 13208 13209 return DurationDisplay; 13210 }(TimeDisplay); 13211 13212 /** 13213 * The text that is added to the `DurationDisplay` for screen reader users. 13214 * 13215 * @type {string} 13216 * @private 13217 */ 13218 13219 13220 DurationDisplay.prototype.labelText_ = 'Duration'; 13221 13222 /** 13223 * The text that should display over the `DurationDisplay`s controls. Added to for localization. 13224 * 13225 * @type {string} 13226 * @private 13227 * 13228 * @deprecated in v7; controlText_ is not used in non-active display Components 13229 */ 13230 DurationDisplay.prototype.controlText_ = 'Duration'; 13231 13232 Component.registerComponent('DurationDisplay', DurationDisplay); 13233 13234 /** 13235 * @file time-divider.js 13236 */ 13237 13238 /** 13239 * The separator between the current time and duration. 13240 * Can be hidden if it's not needed in the design. 13241 * 13242 * @extends Component 13243 */ 13244 13245 var TimeDivider = function (_Component) { 13246 inherits(TimeDivider, _Component); 13247 13248 function TimeDivider() { 13249 classCallCheck(this, TimeDivider); 13250 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 13251 } 13252 13253 /** 13254 * Create the component's DOM element 13255 * 13256 * @return {Element} 13257 * The element that was created. 13258 */ 13259 TimeDivider.prototype.createEl = function createEl() { 13260 return _Component.prototype.createEl.call(this, 'div', { 13261 className: 'vjs-time-control vjs-time-divider', 13262 innerHTML: '<div><span>/</span></div>' 13263 }); 13264 }; 13265 13266 return TimeDivider; 13267 }(Component); 13268 13269 Component.registerComponent('TimeDivider', TimeDivider); 13270 13271 /** 13272 * @file remaining-time-display.js 13273 */ 13274 /** 13275 * Displays the time left in the video 13276 * 13277 * @extends Component 13278 */ 13279 13280 var RemainingTimeDisplay = function (_TimeDisplay) { 13281 inherits(RemainingTimeDisplay, _TimeDisplay); 13282 13283 /** 13284 * Creates an instance of this class. 13285 * 13286 * @param {Player} player 13287 * The `Player` that this class should be attached to. 13288 * 13289 * @param {Object} [options] 13290 * The key/value store of player options. 13291 */ 13292 function RemainingTimeDisplay(player, options) { 13293 classCallCheck(this, RemainingTimeDisplay); 13294 13295 var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); 13296 13297 _this.on(player, 'durationchange', _this.throttledUpdateContent); 13298 _this.on(player, 'ended', _this.handleEnded); 13299 return _this; 13300 } 13301 13302 /** 13303 * Builds the default DOM `className`. 13304 * 13305 * @return {string} 13306 * The DOM `className` for this object. 13307 */ 13308 13309 13310 RemainingTimeDisplay.prototype.buildCSSClass = function buildCSSClass() { 13311 return 'vjs-remaining-time'; 13312 }; 13313 13314 /** 13315 * The remaining time display prefixes numbers with a "minus" character. 13316 * 13317 * @param {number} time 13318 * A numeric time, in seconds. 13319 * 13320 * @return {string} 13321 * A formatted time 13322 * 13323 * @private 13324 */ 13325 13326 13327 RemainingTimeDisplay.prototype.formatTime_ = function formatTime_(time) { 13328 // TODO: The "-" should be decorative, and not announced by a screen reader 13329 return '-' + _TimeDisplay.prototype.formatTime_.call(this, time); 13330 }; 13331 13332 /** 13333 * Update remaining time display. 13334 * 13335 * @param {EventTarget~Event} [event] 13336 * The `timeupdate` or `durationchange` event that caused this to run. 13337 * 13338 * @listens Player#timeupdate 13339 * @listens Player#durationchange 13340 */ 13341 13342 13343 RemainingTimeDisplay.prototype.updateContent = function updateContent(event) { 13344 if (!this.player_.duration()) { 13345 return; 13346 } 13347 13348 // @deprecated We should only use remainingTimeDisplay 13349 // as of video.js 7 13350 if (this.player_.remainingTimeDisplay) { 13351 this.updateFormattedTime_(this.player_.remainingTimeDisplay()); 13352 } else { 13353 this.updateFormattedTime_(this.player_.remainingTime()); 13354 } 13355 }; 13356 13357 /** 13358 * When the player fires ended there should be no time left. Sadly 13359 * this is not always the case, lets make it seem like that is the case 13360 * for users. 13361 * 13362 * @param {EventTarget~Event} [event] 13363 * The `ended` event that caused this to run. 13364 * 13365 * @listens Player#ended 13366 */ 13367 13368 13369 RemainingTimeDisplay.prototype.handleEnded = function handleEnded(event) { 13370 if (!this.player_.duration()) { 13371 return; 13372 } 13373 this.updateFormattedTime_(0); 13374 }; 13375 13376 return RemainingTimeDisplay; 13377 }(TimeDisplay); 13378 13379 /** 13380 * The text that is added to the `RemainingTimeDisplay` for screen reader users. 13381 * 13382 * @type {string} 13383 * @private 13384 */ 13385 13386 13387 RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time'; 13388 13389 /** 13390 * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization. 13391 * 13392 * @type {string} 13393 * @private 13394 * 13395 * @deprecated in v7; controlText_ is not used in non-active display Components 13396 */ 13397 RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time'; 13398 13399 Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); 13400 13401 /** 13402 * @file live-display.js 13403 */ 13404 13405 // TODO - Future make it click to snap to live 13406 13407 /** 13408 * Displays the live indicator when duration is Infinity. 13409 * 13410 * @extends Component 13411 */ 13412 13413 var LiveDisplay = function (_Component) { 13414 inherits(LiveDisplay, _Component); 13415 13416 /** 13417 * Creates an instance of this class. 13418 * 13419 * @param {Player} player 13420 * The `Player` that this class should be attached to. 13421 * 13422 * @param {Object} [options] 13423 * The key/value store of player options. 13424 */ 13425 function LiveDisplay(player, options) { 13426 classCallCheck(this, LiveDisplay); 13427 13428 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 13429 13430 _this.updateShowing(); 13431 _this.on(_this.player(), 'durationchange', _this.updateShowing); 13432 return _this; 13433 } 13434 13435 /** 13436 * Create the `Component`'s DOM element 13437 * 13438 * @return {Element} 13439 * The element that was created. 13440 */ 13441 13442 13443 LiveDisplay.prototype.createEl = function createEl$$1() { 13444 var el = _Component.prototype.createEl.call(this, 'div', { 13445 className: 'vjs-live-control vjs-control' 13446 }); 13447 13448 this.contentEl_ = createEl('div', { 13449 className: 'vjs-live-display', 13450 innerHTML: '<span class="vjs-control-text">' + this.localize('Stream Type') + '\xA0</span>' + this.localize('LIVE') 13451 }, { 13452 'aria-live': 'off' 13453 }); 13454 13455 el.appendChild(this.contentEl_); 13456 return el; 13457 }; 13458 13459 LiveDisplay.prototype.dispose = function dispose() { 13460 this.contentEl_ = null; 13461 13462 _Component.prototype.dispose.call(this); 13463 }; 13464 13465 /** 13466 * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide 13467 * it accordingly 13468 * 13469 * @param {EventTarget~Event} [event] 13470 * The {@link Player#durationchange} event that caused this function to run. 13471 * 13472 * @listens Player#durationchange 13473 */ 13474 13475 13476 LiveDisplay.prototype.updateShowing = function updateShowing(event) { 13477 if (this.player().duration() === Infinity) { 13478 this.show(); 13479 } else { 13480 this.hide(); 13481 } 13482 }; 13483 13484 return LiveDisplay; 13485 }(Component); 13486 13487 Component.registerComponent('LiveDisplay', LiveDisplay); 13488 13489 /** 13490 * @file slider.js 13491 */ 13492 13493 /** 13494 * The base functionality for a slider. Can be vertical or horizontal. 13495 * For instance the volume bar or the seek bar on a video is a slider. 13496 * 13497 * @extends Component 13498 */ 13499 13500 var Slider = function (_Component) { 13501 inherits(Slider, _Component); 13502 13503 /** 13504 * Create an instance of this class 13505 * 13506 * @param {Player} player 13507 * The `Player` that this class should be attached to. 13508 * 13509 * @param {Object} [options] 13510 * The key/value store of player options. 13511 */ 13512 function Slider(player, options) { 13513 classCallCheck(this, Slider); 13514 13515 // Set property names to bar to match with the child Slider class is looking for 13516 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 13517 13518 _this.bar = _this.getChild(_this.options_.barName); 13519 13520 // Set a horizontal or vertical class on the slider depending on the slider type 13521 _this.vertical(!!_this.options_.vertical); 13522 13523 _this.enable(); 13524 return _this; 13525 } 13526 13527 /** 13528 * Are controls are currently enabled for this slider or not. 13529 * 13530 * @return {boolean} 13531 * true if controls are enabled, false otherwise 13532 */ 13533 13534 13535 Slider.prototype.enabled = function enabled() { 13536 return this.enabled_; 13537 }; 13538 13539 /** 13540 * Enable controls for this slider if they are disabled 13541 */ 13542 13543 13544 Slider.prototype.enable = function enable() { 13545 if (this.enabled()) { 13546 return; 13547 } 13548 13549 this.on('mousedown', this.handleMouseDown); 13550 this.on('touchstart', this.handleMouseDown); 13551 this.on('focus', this.handleFocus); 13552 this.on('blur', this.handleBlur); 13553 this.on('click', this.handleClick); 13554 13555 this.on(this.player_, 'controlsvisible', this.update); 13556 13557 if (this.playerEvent) { 13558 this.on(this.player_, this.playerEvent, this.update); 13559 } 13560 13561 this.removeClass('disabled'); 13562 this.setAttribute('tabindex', 0); 13563 13564 this.enabled_ = true; 13565 }; 13566 13567 /** 13568 * Disable controls for this slider if they are enabled 13569 */ 13570 13571 13572 Slider.prototype.disable = function disable() { 13573 if (!this.enabled()) { 13574 return; 13575 } 13576 var doc = this.bar.el_.ownerDocument; 13577 13578 this.off('mousedown', this.handleMouseDown); 13579 this.off('touchstart', this.handleMouseDown); 13580 this.off('focus', this.handleFocus); 13581 this.off('blur', this.handleBlur); 13582 this.off('click', this.handleClick); 13583 this.off(this.player_, 'controlsvisible', this.update); 13584 this.off(doc, 'mousemove', this.handleMouseMove); 13585 this.off(doc, 'mouseup', this.handleMouseUp); 13586 this.off(doc, 'touchmove', this.handleMouseMove); 13587 this.off(doc, 'touchend', this.handleMouseUp); 13588 this.removeAttribute('tabindex'); 13589 13590 this.addClass('disabled'); 13591 13592 if (this.playerEvent) { 13593 this.off(this.player_, this.playerEvent, this.update); 13594 } 13595 this.enabled_ = false; 13596 }; 13597 13598 /** 13599 * Create the `Slider`s DOM element. 13600 * 13601 * @param {string} type 13602 * Type of element to create. 13603 * 13604 * @param {Object} [props={}] 13605 * List of properties in Object form. 13606 * 13607 * @param {Object} [attributes={}] 13608 * list of attributes in Object form. 13609 * 13610 * @return {Element} 13611 * The element that gets created. 13612 */ 13613 13614 13615 Slider.prototype.createEl = function createEl$$1(type) { 13616 var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 13617 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 13618 13619 // Add the slider element class to all sub classes 13620 props.className = props.className + ' vjs-slider'; 13621 props = assign({ 13622 tabIndex: 0 13623 }, props); 13624 13625 attributes = assign({ 13626 'role': 'slider', 13627 'aria-valuenow': 0, 13628 'aria-valuemin': 0, 13629 'aria-valuemax': 100, 13630 'tabIndex': 0 13631 }, attributes); 13632 13633 return _Component.prototype.createEl.call(this, type, props, attributes); 13634 }; 13635 13636 /** 13637 * Handle `mousedown` or `touchstart` events on the `Slider`. 13638 * 13639 * @param {EventTarget~Event} event 13640 * `mousedown` or `touchstart` event that triggered this function 13641 * 13642 * @listens mousedown 13643 * @listens touchstart 13644 * @fires Slider#slideractive 13645 */ 13646 13647 13648 Slider.prototype.handleMouseDown = function handleMouseDown(event) { 13649 var doc = this.bar.el_.ownerDocument; 13650 13651 if (event.type === 'mousedown') { 13652 event.preventDefault(); 13653 } 13654 // Do not call preventDefault() on touchstart in Chrome 13655 // to avoid console warnings. Use a 'touch-action: none' style 13656 // instead to prevent unintented scrolling. 13657 // https://developers.google.com/web/updates/2017/01/scrolling-intervention 13658 if (event.type === 'touchstart' && !IS_CHROME) { 13659 event.preventDefault(); 13660 } 13661 blockTextSelection(); 13662 13663 this.addClass('vjs-sliding'); 13664 /** 13665 * Triggered when the slider is in an active state 13666 * 13667 * @event Slider#slideractive 13668 * @type {EventTarget~Event} 13669 */ 13670 this.trigger('slideractive'); 13671 13672 this.on(doc, 'mousemove', this.handleMouseMove); 13673 this.on(doc, 'mouseup', this.handleMouseUp); 13674 this.on(doc, 'touchmove', this.handleMouseMove); 13675 this.on(doc, 'touchend', this.handleMouseUp); 13676 13677 this.handleMouseMove(event); 13678 }; 13679 13680 /** 13681 * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`. 13682 * The `mousemove` and `touchmove` events will only only trigger this function during 13683 * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and 13684 * {@link Slider#handleMouseUp}. 13685 * 13686 * @param {EventTarget~Event} event 13687 * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered 13688 * this function 13689 * 13690 * @listens mousemove 13691 * @listens touchmove 13692 */ 13693 13694 13695 Slider.prototype.handleMouseMove = function handleMouseMove(event) {}; 13696 13697 /** 13698 * Handle `mouseup` or `touchend` events on the `Slider`. 13699 * 13700 * @param {EventTarget~Event} event 13701 * `mouseup` or `touchend` event that triggered this function. 13702 * 13703 * @listens touchend 13704 * @listens mouseup 13705 * @fires Slider#sliderinactive 13706 */ 13707 13708 13709 Slider.prototype.handleMouseUp = function handleMouseUp() { 13710 var doc = this.bar.el_.ownerDocument; 13711 13712 unblockTextSelection(); 13713 13714 this.removeClass('vjs-sliding'); 13715 /** 13716 * Triggered when the slider is no longer in an active state. 13717 * 13718 * @event Slider#sliderinactive 13719 * @type {EventTarget~Event} 13720 */ 13721 this.trigger('sliderinactive'); 13722 13723 this.off(doc, 'mousemove', this.handleMouseMove); 13724 this.off(doc, 'mouseup', this.handleMouseUp); 13725 this.off(doc, 'touchmove', this.handleMouseMove); 13726 this.off(doc, 'touchend', this.handleMouseUp); 13727 13728 this.update(); 13729 }; 13730 13731 /** 13732 * Update the progress bar of the `Slider`. 13733 * 13734 * @returns {number} 13735 * The percentage of progress the progress bar represents as a 13736 * number from 0 to 1. 13737 */ 13738 13739 13740 Slider.prototype.update = function update() { 13741 13742 // In VolumeBar init we have a setTimeout for update that pops and update 13743 // to the end of the execution stack. The player is destroyed before then 13744 // update will cause an error 13745 if (!this.el_) { 13746 return; 13747 } 13748 13749 // If scrubbing, we could use a cached value to make the handle keep up 13750 // with the user's mouse. On HTML5 browsers scrubbing is really smooth, but 13751 // some flash players are slow, so we might want to utilize this later. 13752 // var progress = (this.player_.scrubbing()) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); 13753 var progress = this.getPercent(); 13754 var bar = this.bar; 13755 13756 // If there's no bar... 13757 if (!bar) { 13758 return; 13759 } 13760 13761 // Protect against no duration and other division issues 13762 if (typeof progress !== 'number' || progress !== progress || progress < 0 || progress === Infinity) { 13763 progress = 0; 13764 } 13765 13766 // Convert to a percentage for setting 13767 var percentage = (progress * 100).toFixed(2) + '%'; 13768 var style = bar.el().style; 13769 13770 // Set the new bar width or height 13771 if (this.vertical()) { 13772 style.height = percentage; 13773 } else { 13774 style.width = percentage; 13775 } 13776 13777 return progress; 13778 }; 13779 13780 /** 13781 * Calculate distance for slider 13782 * 13783 * @param {EventTarget~Event} event 13784 * The event that caused this function to run. 13785 * 13786 * @return {number} 13787 * The current position of the Slider. 13788 * - position.x for vertical `Slider`s 13789 * - position.y for horizontal `Slider`s 13790 */ 13791 13792 13793 Slider.prototype.calculateDistance = function calculateDistance(event) { 13794 var position = getPointerPosition(this.el_, event); 13795 13796 if (this.vertical()) { 13797 return position.y; 13798 } 13799 return position.x; 13800 }; 13801 13802 /** 13803 * Handle a `focus` event on this `Slider`. 13804 * 13805 * @param {EventTarget~Event} event 13806 * The `focus` event that caused this function to run. 13807 * 13808 * @listens focus 13809 */ 13810 13811 13812 Slider.prototype.handleFocus = function handleFocus() { 13813 this.on(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress); 13814 }; 13815 13816 /** 13817 * Handle a `keydown` event on the `Slider`. Watches for left, rigth, up, and down 13818 * arrow keys. This function will only be called when the slider has focus. See 13819 * {@link Slider#handleFocus} and {@link Slider#handleBlur}. 13820 * 13821 * @param {EventTarget~Event} event 13822 * the `keydown` event that caused this function to run. 13823 * 13824 * @listens keydown 13825 */ 13826 13827 13828 Slider.prototype.handleKeyPress = function handleKeyPress(event) { 13829 // Left and Down Arrows 13830 if (event.which === 37 || event.which === 40) { 13831 event.preventDefault(); 13832 this.stepBack(); 13833 13834 // Up and Right Arrows 13835 } else if (event.which === 38 || event.which === 39) { 13836 event.preventDefault(); 13837 this.stepForward(); 13838 } 13839 }; 13840 13841 /** 13842 * Handle a `blur` event on this `Slider`. 13843 * 13844 * @param {EventTarget~Event} event 13845 * The `blur` event that caused this function to run. 13846 * 13847 * @listens blur 13848 */ 13849 13850 Slider.prototype.handleBlur = function handleBlur() { 13851 this.off(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress); 13852 }; 13853 13854 /** 13855 * Listener for click events on slider, used to prevent clicks 13856 * from bubbling up to parent elements like button menus. 13857 * 13858 * @param {Object} event 13859 * Event that caused this object to run 13860 */ 13861 13862 13863 Slider.prototype.handleClick = function handleClick(event) { 13864 event.stopImmediatePropagation(); 13865 event.preventDefault(); 13866 }; 13867 13868 /** 13869 * Get/set if slider is horizontal for vertical 13870 * 13871 * @param {boolean} [bool] 13872 * - true if slider is vertical, 13873 * - false is horizontal 13874 * 13875 * @return {boolean} 13876 * - true if slider is vertical, and getting 13877 * - false if the slider is horizontal, and getting 13878 */ 13879 13880 13881 Slider.prototype.vertical = function vertical(bool) { 13882 if (bool === undefined) { 13883 return this.vertical_ || false; 13884 } 13885 13886 this.vertical_ = !!bool; 13887 13888 if (this.vertical_) { 13889 this.addClass('vjs-slider-vertical'); 13890 } else { 13891 this.addClass('vjs-slider-horizontal'); 13892 } 13893 }; 13894 13895 return Slider; 13896 }(Component); 13897 13898 Component.registerComponent('Slider', Slider); 13899 13900 /** 13901 * @file load-progress-bar.js 13902 */ 13903 13904 /** 13905 * Shows loading progress 13906 * 13907 * @extends Component 13908 */ 13909 13910 var LoadProgressBar = function (_Component) { 13911 inherits(LoadProgressBar, _Component); 13912 13913 /** 13914 * Creates an instance of this class. 13915 * 13916 * @param {Player} player 13917 * The `Player` that this class should be attached to. 13918 * 13919 * @param {Object} [options] 13920 * The key/value store of player options. 13921 */ 13922 function LoadProgressBar(player, options) { 13923 classCallCheck(this, LoadProgressBar); 13924 13925 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 13926 13927 _this.partEls_ = []; 13928 _this.on(player, 'progress', _this.update); 13929 return _this; 13930 } 13931 13932 /** 13933 * Create the `Component`'s DOM element 13934 * 13935 * @return {Element} 13936 * The element that was created. 13937 */ 13938 13939 13940 LoadProgressBar.prototype.createEl = function createEl$$1() { 13941 return _Component.prototype.createEl.call(this, 'div', { 13942 className: 'vjs-load-progress', 13943 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>' 13944 }); 13945 }; 13946 13947 LoadProgressBar.prototype.dispose = function dispose() { 13948 this.partEls_ = null; 13949 13950 _Component.prototype.dispose.call(this); 13951 }; 13952 13953 /** 13954 * Update progress bar 13955 * 13956 * @param {EventTarget~Event} [event] 13957 * The `progress` event that caused this function to run. 13958 * 13959 * @listens Player#progress 13960 */ 13961 13962 13963 LoadProgressBar.prototype.update = function update(event) { 13964 var buffered = this.player_.buffered(); 13965 var duration = this.player_.duration(); 13966 var bufferedEnd = this.player_.bufferedEnd(); 13967 var children = this.partEls_; 13968 13969 // get the percent width of a time compared to the total end 13970 var percentify = function percentify(time, end) { 13971 // no NaN 13972 var percent = time / end || 0; 13973 13974 return (percent >= 1 ? 1 : percent) * 100 + '%'; 13975 }; 13976 13977 // update the width of the progress bar 13978 this.el_.style.width = percentify(bufferedEnd, duration); 13979 13980 // add child elements to represent the individual buffered time ranges 13981 for (var i = 0; i < buffered.length; i++) { 13982 var start = buffered.start(i); 13983 var end = buffered.end(i); 13984 var part = children[i]; 13985 13986 if (!part) { 13987 part = this.el_.appendChild(createEl()); 13988 children[i] = part; 13989 } 13990 13991 // set the percent based on the width of the progress bar (bufferedEnd) 13992 part.style.left = percentify(start, bufferedEnd); 13993 part.style.width = percentify(end - start, bufferedEnd); 13994 } 13995 13996 // remove unused buffered range elements 13997 for (var _i = children.length; _i > buffered.length; _i--) { 13998 this.el_.removeChild(children[_i - 1]); 13999 } 14000 children.length = buffered.length; 14001 }; 14002 14003 return LoadProgressBar; 14004 }(Component); 14005 14006 Component.registerComponent('LoadProgressBar', LoadProgressBar); 14007 14008 /** 14009 * @file time-tooltip.js 14010 */ 14011 14012 /** 14013 * Time tooltips display a time above the progress bar. 14014 * 14015 * @extends Component 14016 */ 14017 14018 var TimeTooltip = function (_Component) { 14019 inherits(TimeTooltip, _Component); 14020 14021 function TimeTooltip() { 14022 classCallCheck(this, TimeTooltip); 14023 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 14024 } 14025 14026 /** 14027 * Create the time tooltip DOM element 14028 * 14029 * @return {Element} 14030 * The element that was created. 14031 */ 14032 TimeTooltip.prototype.createEl = function createEl$$1() { 14033 return _Component.prototype.createEl.call(this, 'div', { 14034 className: 'vjs-time-tooltip' 14035 }); 14036 }; 14037 14038 /** 14039 * Updates the position of the time tooltip relative to the `SeekBar`. 14040 * 14041 * @param {Object} seekBarRect 14042 * The `ClientRect` for the {@link SeekBar} element. 14043 * 14044 * @param {number} seekBarPoint 14045 * A number from 0 to 1, representing a horizontal reference point 14046 * from the left edge of the {@link SeekBar} 14047 */ 14048 14049 14050 TimeTooltip.prototype.update = function update(seekBarRect, seekBarPoint, content) { 14051 var tooltipRect = getBoundingClientRect(this.el_); 14052 var playerRect = getBoundingClientRect(this.player_.el()); 14053 var seekBarPointPx = seekBarRect.width * seekBarPoint; 14054 14055 // do nothing if either rect isn't available 14056 // for example, if the player isn't in the DOM for testing 14057 if (!playerRect || !tooltipRect) { 14058 return; 14059 } 14060 14061 // This is the space left of the `seekBarPoint` available within the bounds 14062 // of the player. We calculate any gap between the left edge of the player 14063 // and the left edge of the `SeekBar` and add the number of pixels in the 14064 // `SeekBar` before hitting the `seekBarPoint` 14065 var spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx; 14066 14067 // This is the space right of the `seekBarPoint` available within the bounds 14068 // of the player. We calculate the number of pixels from the `seekBarPoint` 14069 // to the right edge of the `SeekBar` and add to that any gap between the 14070 // right edge of the `SeekBar` and the player. 14071 var spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right); 14072 14073 // This is the number of pixels by which the tooltip will need to be pulled 14074 // further to the right to center it over the `seekBarPoint`. 14075 var pullTooltipBy = tooltipRect.width / 2; 14076 14077 // Adjust the `pullTooltipBy` distance to the left or right depending on 14078 // the results of the space calculations above. 14079 if (spaceLeftOfPoint < pullTooltipBy) { 14080 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 14081 } else if (spaceRightOfPoint < pullTooltipBy) { 14082 pullTooltipBy = spaceRightOfPoint; 14083 } 14084 14085 // Due to the imprecision of decimal/ratio based calculations and varying 14086 // rounding behaviors, there are cases where the spacing adjustment is off 14087 // by a pixel or two. This adds insurance to these calculations. 14088 if (pullTooltipBy < 0) { 14089 pullTooltipBy = 0; 14090 } else if (pullTooltipBy > tooltipRect.width) { 14091 pullTooltipBy = tooltipRect.width; 14092 } 14093 14094 this.el_.style.right = '-' + pullTooltipBy + 'px'; 14095 textContent(this.el_, content); 14096 }; 14097 14098 return TimeTooltip; 14099 }(Component); 14100 14101 Component.registerComponent('TimeTooltip', TimeTooltip); 14102 14103 /** 14104 * @file play-progress-bar.js 14105 */ 14106 14107 /** 14108 * Used by {@link SeekBar} to display media playback progress as part of the 14109 * {@link ProgressControl}. 14110 * 14111 * @extends Component 14112 */ 14113 14114 var PlayProgressBar = function (_Component) { 14115 inherits(PlayProgressBar, _Component); 14116 14117 function PlayProgressBar() { 14118 classCallCheck(this, PlayProgressBar); 14119 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 14120 } 14121 14122 /** 14123 * Create the the DOM element for this class. 14124 * 14125 * @return {Element} 14126 * The element that was created. 14127 */ 14128 PlayProgressBar.prototype.createEl = function createEl() { 14129 return _Component.prototype.createEl.call(this, 'div', { 14130 className: 'vjs-play-progress vjs-slider-bar', 14131 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>' 14132 }); 14133 }; 14134 14135 /** 14136 * Enqueues updates to its own DOM as well as the DOM of its 14137 * {@link TimeTooltip} child. 14138 * 14139 * @param {Object} seekBarRect 14140 * The `ClientRect` for the {@link SeekBar} element. 14141 * 14142 * @param {number} seekBarPoint 14143 * A number from 0 to 1, representing a horizontal reference point 14144 * from the left edge of the {@link SeekBar} 14145 */ 14146 14147 14148 PlayProgressBar.prototype.update = function update(seekBarRect, seekBarPoint) { 14149 var _this2 = this; 14150 14151 // If there is an existing rAF ID, cancel it so we don't over-queue. 14152 if (this.rafId_) { 14153 this.cancelAnimationFrame(this.rafId_); 14154 } 14155 14156 this.rafId_ = this.requestAnimationFrame(function () { 14157 var time = _this2.player_.scrubbing() ? _this2.player_.getCache().currentTime : _this2.player_.currentTime(); 14158 14159 var content = formatTime(time, _this2.player_.duration()); 14160 var timeTooltip = _this2.getChild('timeTooltip'); 14161 14162 if (timeTooltip) { 14163 timeTooltip.update(seekBarRect, seekBarPoint, content); 14164 } 14165 }); 14166 }; 14167 14168 return PlayProgressBar; 14169 }(Component); 14170 14171 /** 14172 * Default options for {@link PlayProgressBar}. 14173 * 14174 * @type {Object} 14175 * @private 14176 */ 14177 14178 14179 PlayProgressBar.prototype.options_ = { 14180 children: [] 14181 }; 14182 14183 // Time tooltips should not be added to a player on mobile devices 14184 if (!IS_IOS && !IS_ANDROID) { 14185 PlayProgressBar.prototype.options_.children.push('timeTooltip'); 14186 } 14187 14188 Component.registerComponent('PlayProgressBar', PlayProgressBar); 14189 14190 /** 14191 * @file mouse-time-display.js 14192 */ 14193 14194 /** 14195 * The {@link MouseTimeDisplay} component tracks mouse movement over the 14196 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip} 14197 * indicating the time which is represented by a given point in the 14198 * {@link ProgressControl}. 14199 * 14200 * @extends Component 14201 */ 14202 14203 var MouseTimeDisplay = function (_Component) { 14204 inherits(MouseTimeDisplay, _Component); 14205 14206 /** 14207 * Creates an instance of this class. 14208 * 14209 * @param {Player} player 14210 * The {@link Player} that this class should be attached to. 14211 * 14212 * @param {Object} [options] 14213 * The key/value store of player options. 14214 */ 14215 function MouseTimeDisplay(player, options) { 14216 classCallCheck(this, MouseTimeDisplay); 14217 14218 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 14219 14220 _this.update = throttle(bind(_this, _this.update), 25); 14221 return _this; 14222 } 14223 14224 /** 14225 * Create the DOM element for this class. 14226 * 14227 * @return {Element} 14228 * The element that was created. 14229 */ 14230 14231 14232 MouseTimeDisplay.prototype.createEl = function createEl() { 14233 return _Component.prototype.createEl.call(this, 'div', { 14234 className: 'vjs-mouse-display' 14235 }); 14236 }; 14237 14238 /** 14239 * Enqueues updates to its own DOM as well as the DOM of its 14240 * {@link TimeTooltip} child. 14241 * 14242 * @param {Object} seekBarRect 14243 * The `ClientRect` for the {@link SeekBar} element. 14244 * 14245 * @param {number} seekBarPoint 14246 * A number from 0 to 1, representing a horizontal reference point 14247 * from the left edge of the {@link SeekBar} 14248 */ 14249 14250 14251 MouseTimeDisplay.prototype.update = function update(seekBarRect, seekBarPoint) { 14252 var _this2 = this; 14253 14254 // If there is an existing rAF ID, cancel it so we don't over-queue. 14255 if (this.rafId_) { 14256 this.cancelAnimationFrame(this.rafId_); 14257 } 14258 14259 this.rafId_ = this.requestAnimationFrame(function () { 14260 var duration = _this2.player_.duration(); 14261 var content = formatTime(seekBarPoint * duration, duration); 14262 14263 _this2.el_.style.left = seekBarRect.width * seekBarPoint + 'px'; 14264 _this2.getChild('timeTooltip').update(seekBarRect, seekBarPoint, content); 14265 }); 14266 }; 14267 14268 return MouseTimeDisplay; 14269 }(Component); 14270 14271 /** 14272 * Default options for `MouseTimeDisplay` 14273 * 14274 * @type {Object} 14275 * @private 14276 */ 14277 14278 14279 MouseTimeDisplay.prototype.options_ = { 14280 children: ['timeTooltip'] 14281 }; 14282 14283 Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay); 14284 14285 /** 14286 * @file seek-bar.js 14287 */ 14288 14289 // The number of seconds the `step*` functions move the timeline. 14290 var STEP_SECONDS = 5; 14291 14292 // The interval at which the bar should update as it progresses. 14293 var UPDATE_REFRESH_INTERVAL = 30; 14294 14295 /** 14296 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar} 14297 * as its `bar`. 14298 * 14299 * @extends Slider 14300 */ 14301 14302 var SeekBar = function (_Slider) { 14303 inherits(SeekBar, _Slider); 14304 14305 /** 14306 * Creates an instance of this class. 14307 * 14308 * @param {Player} player 14309 * The `Player` that this class should be attached to. 14310 * 14311 * @param {Object} [options] 14312 * The key/value store of player options. 14313 */ 14314 function SeekBar(player, options) { 14315 classCallCheck(this, SeekBar); 14316 14317 var _this = possibleConstructorReturn(this, _Slider.call(this, player, options)); 14318 14319 _this.setEventHandlers_(); 14320 return _this; 14321 } 14322 14323 /** 14324 * Sets the event handlers 14325 * 14326 * @private 14327 */ 14328 14329 14330 SeekBar.prototype.setEventHandlers_ = function setEventHandlers_() { 14331 var _this2 = this; 14332 14333 this.update = throttle(bind(this, this.update), UPDATE_REFRESH_INTERVAL); 14334 14335 this.on(this.player_, 'timeupdate', this.update); 14336 this.on(this.player_, 'ended', this.handleEnded); 14337 14338 // when playing, let's ensure we smoothly update the play progress bar 14339 // via an interval 14340 this.updateInterval = null; 14341 14342 this.on(this.player_, ['playing'], function () { 14343 _this2.clearInterval(_this2.updateInterval); 14344 14345 _this2.updateInterval = _this2.setInterval(function () { 14346 _this2.requestAnimationFrame(function () { 14347 _this2.update(); 14348 }); 14349 }, UPDATE_REFRESH_INTERVAL); 14350 }); 14351 14352 this.on(this.player_, ['ended', 'pause', 'waiting'], function () { 14353 _this2.clearInterval(_this2.updateInterval); 14354 }); 14355 14356 this.on(this.player_, ['timeupdate', 'ended'], this.update); 14357 }; 14358 14359 /** 14360 * Create the `Component`'s DOM element 14361 * 14362 * @return {Element} 14363 * The element that was created. 14364 */ 14365 14366 14367 SeekBar.prototype.createEl = function createEl$$1() { 14368 return _Slider.prototype.createEl.call(this, 'div', { 14369 className: 'vjs-progress-holder' 14370 }, { 14371 'aria-label': this.localize('Progress Bar') 14372 }); 14373 }; 14374 14375 /** 14376 * This function updates the play progress bar and accessibility 14377 * attributes to whatever is passed in. 14378 * 14379 * @param {number} currentTime 14380 * The currentTime value that should be used for accessibility 14381 * 14382 * @param {number} percent 14383 * The percentage as a decimal that the bar should be filled from 0-1. 14384 * 14385 * @private 14386 */ 14387 14388 14389 SeekBar.prototype.update_ = function update_(currentTime, percent) { 14390 var duration = this.player_.duration(); 14391 14392 // machine readable value of progress bar (percentage complete) 14393 this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); 14394 14395 // human readable value of progress bar (time complete) 14396 this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}')); 14397 14398 // Update the `PlayProgressBar`. 14399 this.bar.update(getBoundingClientRect(this.el_), percent); 14400 }; 14401 14402 /** 14403 * Update the seek bar's UI. 14404 * 14405 * @param {EventTarget~Event} [event] 14406 * The `timeupdate` or `ended` event that caused this to run. 14407 * 14408 * @listens Player#timeupdate 14409 * 14410 * @returns {number} 14411 * The current percent at a number from 0-1 14412 */ 14413 14414 14415 SeekBar.prototype.update = function update(event) { 14416 var percent = _Slider.prototype.update.call(this); 14417 14418 this.update_(this.getCurrentTime_(), percent); 14419 return percent; 14420 }; 14421 14422 /** 14423 * Get the value of current time but allows for smooth scrubbing, 14424 * when player can't keep up. 14425 * 14426 * @return {number} 14427 * The current time value to display 14428 * 14429 * @private 14430 */ 14431 14432 14433 SeekBar.prototype.getCurrentTime_ = function getCurrentTime_() { 14434 return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 14435 }; 14436 14437 /** 14438 * We want the seek bar to be full on ended 14439 * no matter what the actual internal values are. so we force it. 14440 * 14441 * @param {EventTarget~Event} [event] 14442 * The `timeupdate` or `ended` event that caused this to run. 14443 * 14444 * @listens Player#ended 14445 */ 14446 14447 14448 SeekBar.prototype.handleEnded = function handleEnded(event) { 14449 this.update_(this.player_.duration(), 1); 14450 }; 14451 14452 /** 14453 * Get the percentage of media played so far. 14454 * 14455 * @return {number} 14456 * The percentage of media played so far (0 to 1). 14457 */ 14458 14459 14460 SeekBar.prototype.getPercent = function getPercent() { 14461 var percent = this.getCurrentTime_() / this.player_.duration(); 14462 14463 return percent >= 1 ? 1 : percent || 0; 14464 }; 14465 14466 /** 14467 * Handle mouse down on seek bar 14468 * 14469 * @param {EventTarget~Event} event 14470 * The `mousedown` event that caused this to run. 14471 * 14472 * @listens mousedown 14473 */ 14474 14475 14476 SeekBar.prototype.handleMouseDown = function handleMouseDown(event) { 14477 if (!isSingleLeftClick(event)) { 14478 return; 14479 } 14480 14481 // Stop event propagation to prevent double fire in progress-control.js 14482 event.stopPropagation(); 14483 this.player_.scrubbing(true); 14484 14485 this.videoWasPlaying = !this.player_.paused(); 14486 this.player_.pause(); 14487 14488 _Slider.prototype.handleMouseDown.call(this, event); 14489 }; 14490 14491 /** 14492 * Handle mouse move on seek bar 14493 * 14494 * @param {EventTarget~Event} event 14495 * The `mousemove` event that caused this to run. 14496 * 14497 * @listens mousemove 14498 */ 14499 14500 14501 SeekBar.prototype.handleMouseMove = function handleMouseMove(event) { 14502 if (!isSingleLeftClick(event)) { 14503 return; 14504 } 14505 14506 var newTime = this.calculateDistance(event) * this.player_.duration(); 14507 14508 // Don't let video end while scrubbing. 14509 if (newTime === this.player_.duration()) { 14510 newTime = newTime - 0.1; 14511 } 14512 14513 // Set new time (tell player to seek to new time) 14514 this.player_.currentTime(newTime); 14515 }; 14516 14517 SeekBar.prototype.enable = function enable() { 14518 _Slider.prototype.enable.call(this); 14519 var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14520 14521 if (!mouseTimeDisplay) { 14522 return; 14523 } 14524 14525 mouseTimeDisplay.show(); 14526 }; 14527 14528 SeekBar.prototype.disable = function disable() { 14529 _Slider.prototype.disable.call(this); 14530 var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14531 14532 if (!mouseTimeDisplay) { 14533 return; 14534 } 14535 14536 mouseTimeDisplay.hide(); 14537 }; 14538 14539 /** 14540 * Handle mouse up on seek bar 14541 * 14542 * @param {EventTarget~Event} event 14543 * The `mouseup` event that caused this to run. 14544 * 14545 * @listens mouseup 14546 */ 14547 14548 14549 SeekBar.prototype.handleMouseUp = function handleMouseUp(event) { 14550 _Slider.prototype.handleMouseUp.call(this, event); 14551 14552 // Stop event propagation to prevent double fire in progress-control.js 14553 if (event) { 14554 event.stopPropagation(); 14555 } 14556 this.player_.scrubbing(false); 14557 14558 /** 14559 * Trigger timeupdate because we're done seeking and the time has changed. 14560 * This is particularly useful for if the player is paused to time the time displays. 14561 * 14562 * @event Tech#timeupdate 14563 * @type {EventTarget~Event} 14564 */ 14565 this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 14566 if (this.videoWasPlaying) { 14567 silencePromise(this.player_.play()); 14568 } 14569 }; 14570 14571 /** 14572 * Move more quickly fast forward for keyboard-only users 14573 */ 14574 14575 14576 SeekBar.prototype.stepForward = function stepForward() { 14577 this.player_.currentTime(this.player_.currentTime() + STEP_SECONDS); 14578 }; 14579 14580 /** 14581 * Move more quickly rewind for keyboard-only users 14582 */ 14583 14584 14585 SeekBar.prototype.stepBack = function stepBack() { 14586 this.player_.currentTime(this.player_.currentTime() - STEP_SECONDS); 14587 }; 14588 14589 /** 14590 * Toggles the playback state of the player 14591 * This gets called when enter or space is used on the seekbar 14592 * 14593 * @param {EventTarget~Event} event 14594 * The `keydown` event that caused this function to be called 14595 * 14596 */ 14597 14598 14599 SeekBar.prototype.handleAction = function handleAction(event) { 14600 if (this.player_.paused()) { 14601 this.player_.play(); 14602 } else { 14603 this.player_.pause(); 14604 } 14605 }; 14606 14607 /** 14608 * Called when this SeekBar has focus and a key gets pressed down. By 14609 * default it will call `this.handleAction` when the key is space or enter. 14610 * 14611 * @param {EventTarget~Event} event 14612 * The `keydown` event that caused this function to be called. 14613 * 14614 * @listens keydown 14615 */ 14616 14617 14618 SeekBar.prototype.handleKeyPress = function handleKeyPress(event) { 14619 14620 // Support Space (32) or Enter (13) key operation to fire a click event 14621 if (event.which === 32 || event.which === 13) { 14622 event.preventDefault(); 14623 this.handleAction(event); 14624 } else if (_Slider.prototype.handleKeyPress) { 14625 14626 // Pass keypress handling up for unsupported keys 14627 _Slider.prototype.handleKeyPress.call(this, event); 14628 } 14629 }; 14630 14631 return SeekBar; 14632 }(Slider); 14633 14634 /** 14635 * Default options for the `SeekBar` 14636 * 14637 * @type {Object} 14638 * @private 14639 */ 14640 14641 14642 SeekBar.prototype.options_ = { 14643 children: ['loadProgressBar', 'playProgressBar'], 14644 barName: 'playProgressBar' 14645 }; 14646 14647 // MouseTimeDisplay tooltips should not be added to a player on mobile devices 14648 if (!IS_IOS && !IS_ANDROID) { 14649 SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); 14650 } 14651 14652 /** 14653 * Call the update event for this Slider when this event happens on the player. 14654 * 14655 * @type {string} 14656 */ 14657 SeekBar.prototype.playerEvent = 'timeupdate'; 14658 14659 Component.registerComponent('SeekBar', SeekBar); 14660 14661 /** 14662 * @file progress-control.js 14663 */ 14664 14665 /** 14666 * The Progress Control component contains the seek bar, load progress, 14667 * and play progress. 14668 * 14669 * @extends Component 14670 */ 14671 14672 var ProgressControl = function (_Component) { 14673 inherits(ProgressControl, _Component); 14674 14675 /** 14676 * Creates an instance of this class. 14677 * 14678 * @param {Player} player 14679 * The `Player` that this class should be attached to. 14680 * 14681 * @param {Object} [options] 14682 * The key/value store of player options. 14683 */ 14684 function ProgressControl(player, options) { 14685 classCallCheck(this, ProgressControl); 14686 14687 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 14688 14689 _this.handleMouseMove = throttle(bind(_this, _this.handleMouseMove), 25); 14690 _this.throttledHandleMouseSeek = throttle(bind(_this, _this.handleMouseSeek), 25); 14691 14692 _this.enable(); 14693 return _this; 14694 } 14695 14696 /** 14697 * Create the `Component`'s DOM element 14698 * 14699 * @return {Element} 14700 * The element that was created. 14701 */ 14702 14703 14704 ProgressControl.prototype.createEl = function createEl$$1() { 14705 return _Component.prototype.createEl.call(this, 'div', { 14706 className: 'vjs-progress-control vjs-control' 14707 }); 14708 }; 14709 14710 /** 14711 * When the mouse moves over the `ProgressControl`, the pointer position 14712 * gets passed down to the `MouseTimeDisplay` component. 14713 * 14714 * @param {EventTarget~Event} event 14715 * The `mousemove` event that caused this function to run. 14716 * 14717 * @listen mousemove 14718 */ 14719 14720 14721 ProgressControl.prototype.handleMouseMove = function handleMouseMove(event) { 14722 var seekBar = this.getChild('seekBar'); 14723 14724 if (seekBar) { 14725 var mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); 14726 var seekBarEl = seekBar.el(); 14727 var seekBarRect = getBoundingClientRect(seekBarEl); 14728 var seekBarPoint = getPointerPosition(seekBarEl, event).x; 14729 14730 // The default skin has a gap on either side of the `SeekBar`. This means 14731 // that it's possible to trigger this behavior outside the boundaries of 14732 // the `SeekBar`. This ensures we stay within it at all times. 14733 if (seekBarPoint > 1) { 14734 seekBarPoint = 1; 14735 } else if (seekBarPoint < 0) { 14736 seekBarPoint = 0; 14737 } 14738 14739 if (mouseTimeDisplay) { 14740 mouseTimeDisplay.update(seekBarRect, seekBarPoint); 14741 } 14742 } 14743 }; 14744 14745 /** 14746 * A throttled version of the {@link ProgressControl#handleMouseSeek} listener. 14747 * 14748 * @method ProgressControl#throttledHandleMouseSeek 14749 * @param {EventTarget~Event} event 14750 * The `mousemove` event that caused this function to run. 14751 * 14752 * @listen mousemove 14753 * @listen touchmove 14754 */ 14755 14756 /** 14757 * Handle `mousemove` or `touchmove` events on the `ProgressControl`. 14758 * 14759 * @param {EventTarget~Event} event 14760 * `mousedown` or `touchstart` event that triggered this function 14761 * 14762 * @listens mousemove 14763 * @listens touchmove 14764 */ 14765 14766 14767 ProgressControl.prototype.handleMouseSeek = function handleMouseSeek(event) { 14768 var seekBar = this.getChild('seekBar'); 14769 14770 if (seekBar) { 14771 seekBar.handleMouseMove(event); 14772 } 14773 }; 14774 14775 /** 14776 * Are controls are currently enabled for this progress control. 14777 * 14778 * @return {boolean} 14779 * true if controls are enabled, false otherwise 14780 */ 14781 14782 14783 ProgressControl.prototype.enabled = function enabled() { 14784 return this.enabled_; 14785 }; 14786 14787 /** 14788 * Disable all controls on the progress control and its children 14789 */ 14790 14791 14792 ProgressControl.prototype.disable = function disable() { 14793 this.children().forEach(function (child) { 14794 return child.disable && child.disable(); 14795 }); 14796 14797 if (!this.enabled()) { 14798 return; 14799 } 14800 14801 this.off(['mousedown', 'touchstart'], this.handleMouseDown); 14802 this.off(this.el_, 'mousemove', this.handleMouseMove); 14803 this.handleMouseUp(); 14804 14805 this.addClass('disabled'); 14806 14807 this.enabled_ = false; 14808 }; 14809 14810 /** 14811 * Enable all controls on the progress control and its children 14812 */ 14813 14814 14815 ProgressControl.prototype.enable = function enable() { 14816 this.children().forEach(function (child) { 14817 return child.enable && child.enable(); 14818 }); 14819 14820 if (this.enabled()) { 14821 return; 14822 } 14823 14824 this.on(['mousedown', 'touchstart'], this.handleMouseDown); 14825 this.on(this.el_, 'mousemove', this.handleMouseMove); 14826 this.removeClass('disabled'); 14827 14828 this.enabled_ = true; 14829 }; 14830 14831 /** 14832 * Handle `mousedown` or `touchstart` events on the `ProgressControl`. 14833 * 14834 * @param {EventTarget~Event} event 14835 * `mousedown` or `touchstart` event that triggered this function 14836 * 14837 * @listens mousedown 14838 * @listens touchstart 14839 */ 14840 14841 14842 ProgressControl.prototype.handleMouseDown = function handleMouseDown(event) { 14843 var doc = this.el_.ownerDocument; 14844 var seekBar = this.getChild('seekBar'); 14845 14846 if (seekBar) { 14847 seekBar.handleMouseDown(event); 14848 } 14849 14850 this.on(doc, 'mousemove', this.throttledHandleMouseSeek); 14851 this.on(doc, 'touchmove', this.throttledHandleMouseSeek); 14852 this.on(doc, 'mouseup', this.handleMouseUp); 14853 this.on(doc, 'touchend', this.handleMouseUp); 14854 }; 14855 14856 /** 14857 * Handle `mouseup` or `touchend` events on the `ProgressControl`. 14858 * 14859 * @param {EventTarget~Event} event 14860 * `mouseup` or `touchend` event that triggered this function. 14861 * 14862 * @listens touchend 14863 * @listens mouseup 14864 */ 14865 14866 14867 ProgressControl.prototype.handleMouseUp = function handleMouseUp(event) { 14868 var doc = this.el_.ownerDocument; 14869 var seekBar = this.getChild('seekBar'); 14870 14871 if (seekBar) { 14872 seekBar.handleMouseUp(event); 14873 } 14874 14875 this.off(doc, 'mousemove', this.throttledHandleMouseSeek); 14876 this.off(doc, 'touchmove', this.throttledHandleMouseSeek); 14877 this.off(doc, 'mouseup', this.handleMouseUp); 14878 this.off(doc, 'touchend', this.handleMouseUp); 14879 }; 14880 14881 return ProgressControl; 14882 }(Component); 14883 14884 /** 14885 * Default options for `ProgressControl` 14886 * 14887 * @type {Object} 14888 * @private 14889 */ 14890 14891 14892 ProgressControl.prototype.options_ = { 14893 children: ['seekBar'] 14894 }; 14895 14896 Component.registerComponent('ProgressControl', ProgressControl); 14897 14898 /** 14899 * @file fullscreen-toggle.js 14900 */ 14901 14902 /** 14903 * Toggle fullscreen video 14904 * 14905 * @extends Button 14906 */ 14907 14908 var FullscreenToggle = function (_Button) { 14909 inherits(FullscreenToggle, _Button); 14910 14911 /** 14912 * Creates an instance of this class. 14913 * 14914 * @param {Player} player 14915 * The `Player` that this class should be attached to. 14916 * 14917 * @param {Object} [options] 14918 * The key/value store of player options. 14919 */ 14920 function FullscreenToggle(player, options) { 14921 classCallCheck(this, FullscreenToggle); 14922 14923 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 14924 14925 _this.on(player, 'fullscreenchange', _this.handleFullscreenChange); 14926 return _this; 14927 } 14928 14929 /** 14930 * Builds the default DOM `className`. 14931 * 14932 * @return {string} 14933 * The DOM `className` for this object. 14934 */ 14935 14936 14937 FullscreenToggle.prototype.buildCSSClass = function buildCSSClass() { 14938 return 'vjs-fullscreen-control ' + _Button.prototype.buildCSSClass.call(this); 14939 }; 14940 14941 /** 14942 * Handles fullscreenchange on the player and change control text accordingly. 14943 * 14944 * @param {EventTarget~Event} [event] 14945 * The {@link Player#fullscreenchange} event that caused this function to be 14946 * called. 14947 * 14948 * @listens Player#fullscreenchange 14949 */ 14950 14951 14952 FullscreenToggle.prototype.handleFullscreenChange = function handleFullscreenChange(event) { 14953 if (this.player_.isFullscreen()) { 14954 this.controlText('Non-Fullscreen'); 14955 } else { 14956 this.controlText('Fullscreen'); 14957 } 14958 }; 14959 14960 /** 14961 * This gets called when an `FullscreenToggle` is "clicked". See 14962 * {@link ClickableComponent} for more detailed information on what a click can be. 14963 * 14964 * @param {EventTarget~Event} [event] 14965 * The `keydown`, `tap`, or `click` event that caused this function to be 14966 * called. 14967 * 14968 * @listens tap 14969 * @listens click 14970 */ 14971 14972 14973 FullscreenToggle.prototype.handleClick = function handleClick(event) { 14974 if (!this.player_.isFullscreen()) { 14975 this.player_.requestFullscreen(); 14976 } else { 14977 this.player_.exitFullscreen(); 14978 } 14979 }; 14980 14981 return FullscreenToggle; 14982 }(Button); 14983 14984 /** 14985 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 14986 * 14987 * @type {string} 14988 * @private 14989 */ 14990 14991 14992 FullscreenToggle.prototype.controlText_ = 'Fullscreen'; 14993 14994 Component.registerComponent('FullscreenToggle', FullscreenToggle); 14995 14996 /** 14997 * Check if volume control is supported and if it isn't hide the 14998 * `Component` that was passed using the `vjs-hidden` class. 14999 * 15000 * @param {Component} self 15001 * The component that should be hidden if volume is unsupported 15002 * 15003 * @param {Player} player 15004 * A reference to the player 15005 * 15006 * @private 15007 */ 15008 var checkVolumeSupport = function checkVolumeSupport(self, player) { 15009 // hide volume controls when they're not supported by the current tech 15010 if (player.tech_ && !player.tech_.featuresVolumeControl) { 15011 self.addClass('vjs-hidden'); 15012 } 15013 15014 self.on(player, 'loadstart', function () { 15015 if (!player.tech_.featuresVolumeControl) { 15016 self.addClass('vjs-hidden'); 15017 } else { 15018 self.removeClass('vjs-hidden'); 15019 } 15020 }); 15021 }; 15022 15023 /** 15024 * @file volume-level.js 15025 */ 15026 15027 /** 15028 * Shows volume level 15029 * 15030 * @extends Component 15031 */ 15032 15033 var VolumeLevel = function (_Component) { 15034 inherits(VolumeLevel, _Component); 15035 15036 function VolumeLevel() { 15037 classCallCheck(this, VolumeLevel); 15038 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 15039 } 15040 15041 /** 15042 * Create the `Component`'s DOM element 15043 * 15044 * @return {Element} 15045 * The element that was created. 15046 */ 15047 VolumeLevel.prototype.createEl = function createEl() { 15048 return _Component.prototype.createEl.call(this, 'div', { 15049 className: 'vjs-volume-level', 15050 innerHTML: '<span class="vjs-control-text"></span>' 15051 }); 15052 }; 15053 15054 return VolumeLevel; 15055 }(Component); 15056 15057 Component.registerComponent('VolumeLevel', VolumeLevel); 15058 15059 /** 15060 * @file volume-bar.js 15061 */ 15062 15063 /** 15064 * The bar that contains the volume level and can be clicked on to adjust the level 15065 * 15066 * @extends Slider 15067 */ 15068 15069 var VolumeBar = function (_Slider) { 15070 inherits(VolumeBar, _Slider); 15071 15072 /** 15073 * Creates an instance of this class. 15074 * 15075 * @param {Player} player 15076 * The `Player` that this class should be attached to. 15077 * 15078 * @param {Object} [options] 15079 * The key/value store of player options. 15080 */ 15081 function VolumeBar(player, options) { 15082 classCallCheck(this, VolumeBar); 15083 15084 var _this = possibleConstructorReturn(this, _Slider.call(this, player, options)); 15085 15086 _this.on('slideractive', _this.updateLastVolume_); 15087 _this.on(player, 'volumechange', _this.updateARIAAttributes); 15088 player.ready(function () { 15089 return _this.updateARIAAttributes(); 15090 }); 15091 return _this; 15092 } 15093 15094 /** 15095 * Create the `Component`'s DOM element 15096 * 15097 * @return {Element} 15098 * The element that was created. 15099 */ 15100 15101 15102 VolumeBar.prototype.createEl = function createEl$$1() { 15103 return _Slider.prototype.createEl.call(this, 'div', { 15104 className: 'vjs-volume-bar vjs-slider-bar' 15105 }, { 15106 'aria-label': this.localize('Volume Level'), 15107 'aria-live': 'polite' 15108 }); 15109 }; 15110 15111 /** 15112 * Handle mouse down on volume bar 15113 * 15114 * @param {EventTarget~Event} event 15115 * The `mousedown` event that caused this to run. 15116 * 15117 * @listens mousedown 15118 */ 15119 15120 15121 VolumeBar.prototype.handleMouseDown = function handleMouseDown(event) { 15122 if (!isSingleLeftClick(event)) { 15123 return; 15124 } 15125 15126 _Slider.prototype.handleMouseDown.call(this, event); 15127 }; 15128 15129 /** 15130 * Handle movement events on the {@link VolumeMenuButton}. 15131 * 15132 * @param {EventTarget~Event} event 15133 * The event that caused this function to run. 15134 * 15135 * @listens mousemove 15136 */ 15137 15138 15139 VolumeBar.prototype.handleMouseMove = function handleMouseMove(event) { 15140 if (!isSingleLeftClick(event)) { 15141 return; 15142 } 15143 15144 this.checkMuted(); 15145 this.player_.volume(this.calculateDistance(event)); 15146 }; 15147 15148 /** 15149 * If the player is muted unmute it. 15150 */ 15151 15152 15153 VolumeBar.prototype.checkMuted = function checkMuted() { 15154 if (this.player_.muted()) { 15155 this.player_.muted(false); 15156 } 15157 }; 15158 15159 /** 15160 * Get percent of volume level 15161 * 15162 * @return {number} 15163 * Volume level percent as a decimal number. 15164 */ 15165 15166 15167 VolumeBar.prototype.getPercent = function getPercent() { 15168 if (this.player_.muted()) { 15169 return 0; 15170 } 15171 return this.player_.volume(); 15172 }; 15173 15174 /** 15175 * Increase volume level for keyboard users 15176 */ 15177 15178 15179 VolumeBar.prototype.stepForward = function stepForward() { 15180 this.checkMuted(); 15181 this.player_.volume(this.player_.volume() + 0.1); 15182 }; 15183 15184 /** 15185 * Decrease volume level for keyboard users 15186 */ 15187 15188 15189 VolumeBar.prototype.stepBack = function stepBack() { 15190 this.checkMuted(); 15191 this.player_.volume(this.player_.volume() - 0.1); 15192 }; 15193 15194 /** 15195 * Update ARIA accessibility attributes 15196 * 15197 * @param {EventTarget~Event} [event] 15198 * The `volumechange` event that caused this function to run. 15199 * 15200 * @listens Player#volumechange 15201 */ 15202 15203 15204 VolumeBar.prototype.updateARIAAttributes = function updateARIAAttributes(event) { 15205 var ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_(); 15206 15207 this.el_.setAttribute('aria-valuenow', ariaValue); 15208 this.el_.setAttribute('aria-valuetext', ariaValue + '%'); 15209 }; 15210 15211 /** 15212 * Returns the current value of the player volume as a percentage 15213 * 15214 * @private 15215 */ 15216 15217 15218 VolumeBar.prototype.volumeAsPercentage_ = function volumeAsPercentage_() { 15219 return Math.round(this.player_.volume() * 100); 15220 }; 15221 15222 /** 15223 * When user starts dragging the VolumeBar, store the volume and listen for 15224 * the end of the drag. When the drag ends, if the volume was set to zero, 15225 * set lastVolume to the stored volume. 15226 * 15227 * @listens slideractive 15228 * @private 15229 */ 15230 15231 15232 VolumeBar.prototype.updateLastVolume_ = function updateLastVolume_() { 15233 var _this2 = this; 15234 15235 var volumeBeforeDrag = this.player_.volume(); 15236 15237 this.one('sliderinactive', function () { 15238 if (_this2.player_.volume() === 0) { 15239 _this2.player_.lastVolume_(volumeBeforeDrag); 15240 } 15241 }); 15242 }; 15243 15244 return VolumeBar; 15245 }(Slider); 15246 15247 /** 15248 * Default options for the `VolumeBar` 15249 * 15250 * @type {Object} 15251 * @private 15252 */ 15253 15254 15255 VolumeBar.prototype.options_ = { 15256 children: ['volumeLevel'], 15257 barName: 'volumeLevel' 15258 }; 15259 15260 /** 15261 * Call the update event for this Slider when this event happens on the player. 15262 * 15263 * @type {string} 15264 */ 15265 VolumeBar.prototype.playerEvent = 'volumechange'; 15266 15267 Component.registerComponent('VolumeBar', VolumeBar); 15268 15269 /** 15270 * @file volume-control.js 15271 */ 15272 15273 /** 15274 * The component for controlling the volume level 15275 * 15276 * @extends Component 15277 */ 15278 15279 var VolumeControl = function (_Component) { 15280 inherits(VolumeControl, _Component); 15281 15282 /** 15283 * Creates an instance of this class. 15284 * 15285 * @param {Player} player 15286 * The `Player` that this class should be attached to. 15287 * 15288 * @param {Object} [options={}] 15289 * The key/value store of player options. 15290 */ 15291 function VolumeControl(player) { 15292 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 15293 classCallCheck(this, VolumeControl); 15294 15295 options.vertical = options.vertical || false; 15296 15297 // Pass the vertical option down to the VolumeBar if 15298 // the VolumeBar is turned on. 15299 if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) { 15300 options.volumeBar = options.volumeBar || {}; 15301 options.volumeBar.vertical = options.vertical; 15302 } 15303 15304 // hide this control if volume support is missing 15305 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15306 15307 checkVolumeSupport(_this, player); 15308 15309 _this.throttledHandleMouseMove = throttle(bind(_this, _this.handleMouseMove), 25); 15310 15311 _this.on('mousedown', _this.handleMouseDown); 15312 _this.on('touchstart', _this.handleMouseDown); 15313 15314 // while the slider is active (the mouse has been pressed down and 15315 // is dragging) or in focus we do not want to hide the VolumeBar 15316 _this.on(_this.volumeBar, ['focus', 'slideractive'], function () { 15317 _this.volumeBar.addClass('vjs-slider-active'); 15318 _this.addClass('vjs-slider-active'); 15319 _this.trigger('slideractive'); 15320 }); 15321 15322 _this.on(_this.volumeBar, ['blur', 'sliderinactive'], function () { 15323 _this.volumeBar.removeClass('vjs-slider-active'); 15324 _this.removeClass('vjs-slider-active'); 15325 _this.trigger('sliderinactive'); 15326 }); 15327 return _this; 15328 } 15329 15330 /** 15331 * Create the `Component`'s DOM element 15332 * 15333 * @return {Element} 15334 * The element that was created. 15335 */ 15336 15337 15338 VolumeControl.prototype.createEl = function createEl() { 15339 var orientationClass = 'vjs-volume-horizontal'; 15340 15341 if (this.options_.vertical) { 15342 orientationClass = 'vjs-volume-vertical'; 15343 } 15344 15345 return _Component.prototype.createEl.call(this, 'div', { 15346 className: 'vjs-volume-control vjs-control ' + orientationClass 15347 }); 15348 }; 15349 15350 /** 15351 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 15352 * 15353 * @param {EventTarget~Event} event 15354 * `mousedown` or `touchstart` event that triggered this function 15355 * 15356 * @listens mousedown 15357 * @listens touchstart 15358 */ 15359 15360 15361 VolumeControl.prototype.handleMouseDown = function handleMouseDown(event) { 15362 var doc = this.el_.ownerDocument; 15363 15364 this.on(doc, 'mousemove', this.throttledHandleMouseMove); 15365 this.on(doc, 'touchmove', this.throttledHandleMouseMove); 15366 this.on(doc, 'mouseup', this.handleMouseUp); 15367 this.on(doc, 'touchend', this.handleMouseUp); 15368 }; 15369 15370 /** 15371 * Handle `mouseup` or `touchend` events on the `VolumeControl`. 15372 * 15373 * @param {EventTarget~Event} event 15374 * `mouseup` or `touchend` event that triggered this function. 15375 * 15376 * @listens touchend 15377 * @listens mouseup 15378 */ 15379 15380 15381 VolumeControl.prototype.handleMouseUp = function handleMouseUp(event) { 15382 var doc = this.el_.ownerDocument; 15383 15384 this.off(doc, 'mousemove', this.throttledHandleMouseMove); 15385 this.off(doc, 'touchmove', this.throttledHandleMouseMove); 15386 this.off(doc, 'mouseup', this.handleMouseUp); 15387 this.off(doc, 'touchend', this.handleMouseUp); 15388 }; 15389 15390 /** 15391 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 15392 * 15393 * @param {EventTarget~Event} event 15394 * `mousedown` or `touchstart` event that triggered this function 15395 * 15396 * @listens mousedown 15397 * @listens touchstart 15398 */ 15399 15400 15401 VolumeControl.prototype.handleMouseMove = function handleMouseMove(event) { 15402 this.volumeBar.handleMouseMove(event); 15403 }; 15404 15405 return VolumeControl; 15406 }(Component); 15407 15408 /** 15409 * Default options for the `VolumeControl` 15410 * 15411 * @type {Object} 15412 * @private 15413 */ 15414 15415 15416 VolumeControl.prototype.options_ = { 15417 children: ['volumeBar'] 15418 }; 15419 15420 Component.registerComponent('VolumeControl', VolumeControl); 15421 15422 /** 15423 * @file mute-toggle.js 15424 */ 15425 15426 /** 15427 * A button component for muting the audio. 15428 * 15429 * @extends Button 15430 */ 15431 15432 var MuteToggle = function (_Button) { 15433 inherits(MuteToggle, _Button); 15434 15435 /** 15436 * Creates an instance of this class. 15437 * 15438 * @param {Player} player 15439 * The `Player` that this class should be attached to. 15440 * 15441 * @param {Object} [options] 15442 * The key/value store of player options. 15443 */ 15444 function MuteToggle(player, options) { 15445 classCallCheck(this, MuteToggle); 15446 15447 // hide this control if volume support is missing 15448 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 15449 15450 checkVolumeSupport(_this, player); 15451 15452 _this.on(player, ['loadstart', 'volumechange'], _this.update); 15453 return _this; 15454 } 15455 15456 /** 15457 * Builds the default DOM `className`. 15458 * 15459 * @return {string} 15460 * The DOM `className` for this object. 15461 */ 15462 15463 15464 MuteToggle.prototype.buildCSSClass = function buildCSSClass() { 15465 return 'vjs-mute-control ' + _Button.prototype.buildCSSClass.call(this); 15466 }; 15467 15468 /** 15469 * This gets called when an `MuteToggle` is "clicked". See 15470 * {@link ClickableComponent} for more detailed information on what a click can be. 15471 * 15472 * @param {EventTarget~Event} [event] 15473 * The `keydown`, `tap`, or `click` event that caused this function to be 15474 * called. 15475 * 15476 * @listens tap 15477 * @listens click 15478 */ 15479 15480 15481 MuteToggle.prototype.handleClick = function handleClick(event) { 15482 var vol = this.player_.volume(); 15483 var lastVolume = this.player_.lastVolume_(); 15484 15485 if (vol === 0) { 15486 var volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume; 15487 15488 this.player_.volume(volumeToSet); 15489 this.player_.muted(false); 15490 } else { 15491 this.player_.muted(this.player_.muted() ? false : true); 15492 } 15493 }; 15494 15495 /** 15496 * Update the `MuteToggle` button based on the state of `volume` and `muted` 15497 * on the player. 15498 * 15499 * @param {EventTarget~Event} [event] 15500 * The {@link Player#loadstart} event if this function was called 15501 * through an event. 15502 * 15503 * @listens Player#loadstart 15504 * @listens Player#volumechange 15505 */ 15506 15507 15508 MuteToggle.prototype.update = function update(event) { 15509 this.updateIcon_(); 15510 this.updateControlText_(); 15511 }; 15512 15513 /** 15514 * Update the appearance of the `MuteToggle` icon. 15515 * 15516 * Possible states (given `level` variable below): 15517 * - 0: crossed out 15518 * - 1: zero bars of volume 15519 * - 2: one bar of volume 15520 * - 3: two bars of volume 15521 * 15522 * @private 15523 */ 15524 15525 15526 MuteToggle.prototype.updateIcon_ = function updateIcon_() { 15527 var vol = this.player_.volume(); 15528 var level = 3; 15529 15530 if (vol === 0 || this.player_.muted()) { 15531 level = 0; 15532 } else if (vol < 0.33) { 15533 level = 1; 15534 } else if (vol < 0.67) { 15535 level = 2; 15536 } 15537 15538 // TODO improve muted icon classes 15539 for (var i = 0; i < 4; i++) { 15540 removeClass(this.el_, 'vjs-vol-' + i); 15541 } 15542 addClass(this.el_, 'vjs-vol-' + level); 15543 }; 15544 15545 /** 15546 * If `muted` has changed on the player, update the control text 15547 * (`title` attribute on `vjs-mute-control` element and content of 15548 * `vjs-control-text` element). 15549 * 15550 * @private 15551 */ 15552 15553 15554 MuteToggle.prototype.updateControlText_ = function updateControlText_() { 15555 var soundOff = this.player_.muted() || this.player_.volume() === 0; 15556 var text = soundOff ? 'Unmute' : 'Mute'; 15557 15558 if (this.controlText() !== text) { 15559 this.controlText(text); 15560 } 15561 }; 15562 15563 return MuteToggle; 15564 }(Button); 15565 15566 /** 15567 * The text that should display over the `MuteToggle`s controls. Added for localization. 15568 * 15569 * @type {string} 15570 * @private 15571 */ 15572 15573 15574 MuteToggle.prototype.controlText_ = 'Mute'; 15575 15576 Component.registerComponent('MuteToggle', MuteToggle); 15577 15578 /** 15579 * @file volume-control.js 15580 */ 15581 15582 /** 15583 * A Component to contain the MuteToggle and VolumeControl so that 15584 * they can work together. 15585 * 15586 * @extends Component 15587 */ 15588 15589 var VolumePanel = function (_Component) { 15590 inherits(VolumePanel, _Component); 15591 15592 /** 15593 * Creates an instance of this class. 15594 * 15595 * @param {Player} player 15596 * The `Player` that this class should be attached to. 15597 * 15598 * @param {Object} [options={}] 15599 * The key/value store of player options. 15600 */ 15601 function VolumePanel(player) { 15602 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 15603 classCallCheck(this, VolumePanel); 15604 15605 if (typeof options.inline !== 'undefined') { 15606 options.inline = options.inline; 15607 } else { 15608 options.inline = true; 15609 } 15610 15611 // pass the inline option down to the VolumeControl as vertical if 15612 // the VolumeControl is on. 15613 if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) { 15614 options.volumeControl = options.volumeControl || {}; 15615 options.volumeControl.vertical = !options.inline; 15616 } 15617 15618 // hide this control if volume support is missing 15619 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15620 15621 checkVolumeSupport(_this, player); 15622 15623 // while the slider is active (the mouse has been pressed down and 15624 // is dragging) we do not want to hide the VolumeBar 15625 _this.on(_this.volumeControl, ['slideractive'], _this.sliderActive_); 15626 15627 _this.on(_this.volumeControl, ['sliderinactive'], _this.sliderInactive_); 15628 return _this; 15629 } 15630 15631 /** 15632 * Add vjs-slider-active class to the VolumePanel 15633 * 15634 * @listens VolumeControl#slideractive 15635 * @private 15636 */ 15637 15638 15639 VolumePanel.prototype.sliderActive_ = function sliderActive_() { 15640 this.addClass('vjs-slider-active'); 15641 }; 15642 15643 /** 15644 * Removes vjs-slider-active class to the VolumePanel 15645 * 15646 * @listens VolumeControl#sliderinactive 15647 * @private 15648 */ 15649 15650 15651 VolumePanel.prototype.sliderInactive_ = function sliderInactive_() { 15652 this.removeClass('vjs-slider-active'); 15653 }; 15654 15655 /** 15656 * Create the `Component`'s DOM element 15657 * 15658 * @return {Element} 15659 * The element that was created. 15660 */ 15661 15662 15663 VolumePanel.prototype.createEl = function createEl() { 15664 var orientationClass = 'vjs-volume-panel-horizontal'; 15665 15666 if (!this.options_.inline) { 15667 orientationClass = 'vjs-volume-panel-vertical'; 15668 } 15669 15670 return _Component.prototype.createEl.call(this, 'div', { 15671 className: 'vjs-volume-panel vjs-control ' + orientationClass 15672 }); 15673 }; 15674 15675 return VolumePanel; 15676 }(Component); 15677 15678 /** 15679 * Default options for the `VolumeControl` 15680 * 15681 * @type {Object} 15682 * @private 15683 */ 15684 15685 15686 VolumePanel.prototype.options_ = { 15687 children: ['muteToggle', 'volumeControl'] 15688 }; 15689 15690 Component.registerComponent('VolumePanel', VolumePanel); 15691 15692 /** 15693 * @file menu.js 15694 */ 15695 15696 /** 15697 * The Menu component is used to build popup menus, including subtitle and 15698 * captions selection menus. 15699 * 15700 * @extends Component 15701 */ 15702 15703 var Menu = function (_Component) { 15704 inherits(Menu, _Component); 15705 15706 /** 15707 * Create an instance of this class. 15708 * 15709 * @param {Player} player 15710 * the player that this component should attach to 15711 * 15712 * @param {Object} [options] 15713 * Object of option names and values 15714 * 15715 */ 15716 function Menu(player, options) { 15717 classCallCheck(this, Menu); 15718 15719 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15720 15721 if (options) { 15722 _this.menuButton_ = options.menuButton; 15723 } 15724 15725 _this.focusedChild_ = -1; 15726 15727 _this.on('keydown', _this.handleKeyPress); 15728 return _this; 15729 } 15730 15731 /** 15732 * Add a {@link MenuItem} to the menu. 15733 * 15734 * @param {Object|string} component 15735 * The name or instance of the `MenuItem` to add. 15736 * 15737 */ 15738 15739 15740 Menu.prototype.addItem = function addItem(component) { 15741 this.addChild(component); 15742 component.on('click', bind(this, function (event) { 15743 // Unpress the associated MenuButton, and move focus back to it 15744 if (this.menuButton_) { 15745 this.menuButton_.unpressButton(); 15746 15747 // don't focus menu button if item is a caption settings item 15748 // because focus will move elsewhere 15749 if (component.name() !== 'CaptionSettingsMenuItem') { 15750 this.menuButton_.focus(); 15751 } 15752 } 15753 })); 15754 }; 15755 15756 /** 15757 * Create the `Menu`s DOM element. 15758 * 15759 * @return {Element} 15760 * the element that was created 15761 */ 15762 15763 15764 Menu.prototype.createEl = function createEl$$1() { 15765 var contentElType = this.options_.contentElType || 'ul'; 15766 15767 this.contentEl_ = createEl(contentElType, { 15768 className: 'vjs-menu-content' 15769 }); 15770 15771 this.contentEl_.setAttribute('role', 'menu'); 15772 15773 var el = _Component.prototype.createEl.call(this, 'div', { 15774 append: this.contentEl_, 15775 className: 'vjs-menu' 15776 }); 15777 15778 el.appendChild(this.contentEl_); 15779 15780 // Prevent clicks from bubbling up. Needed for Menu Buttons, 15781 // where a click on the parent is significant 15782 on(el, 'click', function (event) { 15783 event.preventDefault(); 15784 event.stopImmediatePropagation(); 15785 }); 15786 15787 return el; 15788 }; 15789 15790 Menu.prototype.dispose = function dispose() { 15791 this.contentEl_ = null; 15792 15793 _Component.prototype.dispose.call(this); 15794 }; 15795 15796 /** 15797 * Handle a `keydown` event on this menu. This listener is added in the constructor. 15798 * 15799 * @param {EventTarget~Event} event 15800 * A `keydown` event that happened on the menu. 15801 * 15802 * @listens keydown 15803 */ 15804 15805 15806 Menu.prototype.handleKeyPress = function handleKeyPress(event) { 15807 // Left and Down Arrows 15808 if (event.which === 37 || event.which === 40) { 15809 event.preventDefault(); 15810 this.stepForward(); 15811 15812 // Up and Right Arrows 15813 } else if (event.which === 38 || event.which === 39) { 15814 event.preventDefault(); 15815 this.stepBack(); 15816 } 15817 }; 15818 15819 /** 15820 * Move to next (lower) menu item for keyboard users. 15821 */ 15822 15823 15824 Menu.prototype.stepForward = function stepForward() { 15825 var stepChild = 0; 15826 15827 if (this.focusedChild_ !== undefined) { 15828 stepChild = this.focusedChild_ + 1; 15829 } 15830 this.focus(stepChild); 15831 }; 15832 15833 /** 15834 * Move to previous (higher) menu item for keyboard users. 15835 */ 15836 15837 15838 Menu.prototype.stepBack = function stepBack() { 15839 var stepChild = 0; 15840 15841 if (this.focusedChild_ !== undefined) { 15842 stepChild = this.focusedChild_ - 1; 15843 } 15844 this.focus(stepChild); 15845 }; 15846 15847 /** 15848 * Set focus on a {@link MenuItem} in the `Menu`. 15849 * 15850 * @param {Object|string} [item=0] 15851 * Index of child item set focus on. 15852 */ 15853 15854 15855 Menu.prototype.focus = function focus() { 15856 var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 15857 15858 var children = this.children().slice(); 15859 var haveTitle = children.length && children[0].className && /vjs-menu-title/.test(children[0].className); 15860 15861 if (haveTitle) { 15862 children.shift(); 15863 } 15864 15865 if (children.length > 0) { 15866 if (item < 0) { 15867 item = 0; 15868 } else if (item >= children.length) { 15869 item = children.length - 1; 15870 } 15871 15872 this.focusedChild_ = item; 15873 15874 children[item].el_.focus(); 15875 } 15876 }; 15877 15878 return Menu; 15879 }(Component); 15880 15881 Component.registerComponent('Menu', Menu); 15882 15883 /** 15884 * @file menu-button.js 15885 */ 15886 15887 /** 15888 * A `MenuButton` class for any popup {@link Menu}. 15889 * 15890 * @extends Component 15891 */ 15892 15893 var MenuButton = function (_Component) { 15894 inherits(MenuButton, _Component); 15895 15896 /** 15897 * Creates an instance of this class. 15898 * 15899 * @param {Player} player 15900 * The `Player` that this class should be attached to. 15901 * 15902 * @param {Object} [options={}] 15903 * The key/value store of player options. 15904 */ 15905 function MenuButton(player) { 15906 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 15907 classCallCheck(this, MenuButton); 15908 15909 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15910 15911 _this.menuButton_ = new Button(player, options); 15912 15913 _this.menuButton_.controlText(_this.controlText_); 15914 _this.menuButton_.el_.setAttribute('aria-haspopup', 'true'); 15915 15916 // Add buildCSSClass values to the button, not the wrapper 15917 var buttonClass = Button.prototype.buildCSSClass(); 15918 15919 _this.menuButton_.el_.className = _this.buildCSSClass() + ' ' + buttonClass; 15920 _this.menuButton_.removeClass('vjs-control'); 15921 15922 _this.addChild(_this.menuButton_); 15923 15924 _this.update(); 15925 15926 _this.enabled_ = true; 15927 15928 _this.on(_this.menuButton_, 'tap', _this.handleClick); 15929 _this.on(_this.menuButton_, 'click', _this.handleClick); 15930 _this.on(_this.menuButton_, 'focus', _this.handleFocus); 15931 _this.on(_this.menuButton_, 'blur', _this.handleBlur); 15932 15933 _this.on('keydown', _this.handleSubmenuKeyPress); 15934 return _this; 15935 } 15936 15937 /** 15938 * Update the menu based on the current state of its items. 15939 */ 15940 15941 15942 MenuButton.prototype.update = function update() { 15943 var menu = this.createMenu(); 15944 15945 if (this.menu) { 15946 this.menu.dispose(); 15947 this.removeChild(this.menu); 15948 } 15949 15950 this.menu = menu; 15951 this.addChild(menu); 15952 15953 /** 15954 * Track the state of the menu button 15955 * 15956 * @type {Boolean} 15957 * @private 15958 */ 15959 this.buttonPressed_ = false; 15960 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 15961 15962 if (this.items && this.items.length <= this.hideThreshold_) { 15963 this.hide(); 15964 } else { 15965 this.show(); 15966 } 15967 }; 15968 15969 /** 15970 * Create the menu and add all items to it. 15971 * 15972 * @return {Menu} 15973 * The constructed menu 15974 */ 15975 15976 15977 MenuButton.prototype.createMenu = function createMenu() { 15978 var menu = new Menu(this.player_, { menuButton: this }); 15979 15980 /** 15981 * Hide the menu if the number of items is less than or equal to this threshold. This defaults 15982 * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list 15983 * it here because every time we run `createMenu` we need to reset the value. 15984 * 15985 * @protected 15986 * @type {Number} 15987 */ 15988 this.hideThreshold_ = 0; 15989 15990 // Add a title list item to the top 15991 if (this.options_.title) { 15992 var title = createEl('li', { 15993 className: 'vjs-menu-title', 15994 innerHTML: toTitleCase(this.options_.title), 15995 tabIndex: -1 15996 }); 15997 15998 this.hideThreshold_ += 1; 15999 16000 menu.children_.unshift(title); 16001 prependTo(title, menu.contentEl()); 16002 } 16003 16004 this.items = this.createItems(); 16005 16006 if (this.items) { 16007 // Add menu items to the menu 16008 for (var i = 0; i < this.items.length; i++) { 16009 menu.addItem(this.items[i]); 16010 } 16011 } 16012 16013 return menu; 16014 }; 16015 16016 /** 16017 * Create the list of menu items. Specific to each subclass. 16018 * 16019 * @abstract 16020 */ 16021 16022 16023 MenuButton.prototype.createItems = function createItems() {}; 16024 16025 /** 16026 * Create the `MenuButtons`s DOM element. 16027 * 16028 * @return {Element} 16029 * The element that gets created. 16030 */ 16031 16032 16033 MenuButton.prototype.createEl = function createEl$$1() { 16034 return _Component.prototype.createEl.call(this, 'div', { 16035 className: this.buildWrapperCSSClass() 16036 }, {}); 16037 }; 16038 16039 /** 16040 * Allow sub components to stack CSS class names for the wrapper element 16041 * 16042 * @return {string} 16043 * The constructed wrapper DOM `className` 16044 */ 16045 16046 16047 MenuButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 16048 var menuButtonClass = 'vjs-menu-button'; 16049 16050 // If the inline option is passed, we want to use different styles altogether. 16051 if (this.options_.inline === true) { 16052 menuButtonClass += '-inline'; 16053 } else { 16054 menuButtonClass += '-popup'; 16055 } 16056 16057 // TODO: Fix the CSS so that this isn't necessary 16058 var buttonClass = Button.prototype.buildCSSClass(); 16059 16060 return 'vjs-menu-button ' + menuButtonClass + ' ' + buttonClass + ' ' + _Component.prototype.buildCSSClass.call(this); 16061 }; 16062 16063 /** 16064 * Builds the default DOM `className`. 16065 * 16066 * @return {string} 16067 * The DOM `className` for this object. 16068 */ 16069 16070 16071 MenuButton.prototype.buildCSSClass = function buildCSSClass() { 16072 var menuButtonClass = 'vjs-menu-button'; 16073 16074 // If the inline option is passed, we want to use different styles altogether. 16075 if (this.options_.inline === true) { 16076 menuButtonClass += '-inline'; 16077 } else { 16078 menuButtonClass += '-popup'; 16079 } 16080 16081 return 'vjs-menu-button ' + menuButtonClass + ' ' + _Component.prototype.buildCSSClass.call(this); 16082 }; 16083 16084 /** 16085 * Get or set the localized control text that will be used for accessibility. 16086 * 16087 * > NOTE: This will come from the internal `menuButton_` element. 16088 * 16089 * @param {string} [text] 16090 * Control text for element. 16091 * 16092 * @param {Element} [el=this.menuButton_.el()] 16093 * Element to set the title on. 16094 * 16095 * @return {string} 16096 * - The control text when getting 16097 */ 16098 16099 16100 MenuButton.prototype.controlText = function controlText(text) { 16101 var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.menuButton_.el(); 16102 16103 return this.menuButton_.controlText(text, el); 16104 }; 16105 16106 /** 16107 * Handle a click on a `MenuButton`. 16108 * See {@link ClickableComponent#handleClick} for instances where this is called. 16109 * 16110 * @param {EventTarget~Event} event 16111 * The `keydown`, `tap`, or `click` event that caused this function to be 16112 * called. 16113 * 16114 * @listens tap 16115 * @listens click 16116 */ 16117 16118 16119 MenuButton.prototype.handleClick = function handleClick(event) { 16120 // When you click the button it adds focus, which will show the menu. 16121 // So we'll remove focus when the mouse leaves the button. Focus is needed 16122 // for tab navigation. 16123 16124 this.one(this.menu.contentEl(), 'mouseleave', bind(this, function (e) { 16125 this.unpressButton(); 16126 this.el_.blur(); 16127 })); 16128 if (this.buttonPressed_) { 16129 this.unpressButton(); 16130 } else { 16131 this.pressButton(); 16132 } 16133 }; 16134 16135 /** 16136 * Set the focus to the actual button, not to this element 16137 */ 16138 16139 16140 MenuButton.prototype.focus = function focus() { 16141 this.menuButton_.focus(); 16142 }; 16143 16144 /** 16145 * Remove the focus from the actual button, not this element 16146 */ 16147 16148 16149 MenuButton.prototype.blur = function blur() { 16150 this.menuButton_.blur(); 16151 }; 16152 16153 /** 16154 * This gets called when a `MenuButton` gains focus via a `focus` event. 16155 * Turns on listening for `keydown` events. When they happen it 16156 * calls `this.handleKeyPress`. 16157 * 16158 * @param {EventTarget~Event} event 16159 * The `focus` event that caused this function to be called. 16160 * 16161 * @listens focus 16162 */ 16163 16164 16165 MenuButton.prototype.handleFocus = function handleFocus() { 16166 on(document_1, 'keydown', bind(this, this.handleKeyPress)); 16167 }; 16168 16169 /** 16170 * Called when a `MenuButton` loses focus. Turns off the listener for 16171 * `keydown` events. Which Stops `this.handleKeyPress` from getting called. 16172 * 16173 * @param {EventTarget~Event} event 16174 * The `blur` event that caused this function to be called. 16175 * 16176 * @listens blur 16177 */ 16178 16179 16180 MenuButton.prototype.handleBlur = function handleBlur() { 16181 off(document_1, 'keydown', bind(this, this.handleKeyPress)); 16182 }; 16183 16184 /** 16185 * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See 16186 * {@link ClickableComponent#handleKeyPress} for instances where this is called. 16187 * 16188 * @param {EventTarget~Event} event 16189 * The `keydown` event that caused this function to be called. 16190 * 16191 * @listens keydown 16192 */ 16193 16194 16195 MenuButton.prototype.handleKeyPress = function handleKeyPress(event) { 16196 16197 // Escape (27) key or Tab (9) key unpress the 'button' 16198 if (event.which === 27 || event.which === 9) { 16199 if (this.buttonPressed_) { 16200 this.unpressButton(); 16201 } 16202 // Don't preventDefault for Tab key - we still want to lose focus 16203 if (event.which !== 9) { 16204 event.preventDefault(); 16205 // Set focus back to the menu button's button 16206 this.menuButton_.el_.focus(); 16207 } 16208 // Up (38) key or Down (40) key press the 'button' 16209 } else if (event.which === 38 || event.which === 40) { 16210 if (!this.buttonPressed_) { 16211 this.pressButton(); 16212 event.preventDefault(); 16213 } 16214 } 16215 }; 16216 16217 /** 16218 * Handle a `keydown` event on a sub-menu. The listener for this is added in 16219 * the constructor. 16220 * 16221 * @param {EventTarget~Event} event 16222 * Key press event 16223 * 16224 * @listens keydown 16225 */ 16226 16227 16228 MenuButton.prototype.handleSubmenuKeyPress = function handleSubmenuKeyPress(event) { 16229 16230 // Escape (27) key or Tab (9) key unpress the 'button' 16231 if (event.which === 27 || event.which === 9) { 16232 if (this.buttonPressed_) { 16233 this.unpressButton(); 16234 } 16235 // Don't preventDefault for Tab key - we still want to lose focus 16236 if (event.which !== 9) { 16237 event.preventDefault(); 16238 // Set focus back to the menu button's button 16239 this.menuButton_.el_.focus(); 16240 } 16241 } 16242 }; 16243 16244 /** 16245 * Put the current `MenuButton` into a pressed state. 16246 */ 16247 16248 16249 MenuButton.prototype.pressButton = function pressButton() { 16250 if (this.enabled_) { 16251 this.buttonPressed_ = true; 16252 this.menu.lockShowing(); 16253 this.menuButton_.el_.setAttribute('aria-expanded', 'true'); 16254 16255 // set the focus into the submenu, except on iOS where it is resulting in 16256 // undesired scrolling behavior when the player is in an iframe 16257 if (IS_IOS && isInFrame()) { 16258 // Return early so that the menu isn't focused 16259 return; 16260 } 16261 16262 this.menu.focus(); 16263 } 16264 }; 16265 16266 /** 16267 * Take the current `MenuButton` out of a pressed state. 16268 */ 16269 16270 16271 MenuButton.prototype.unpressButton = function unpressButton() { 16272 if (this.enabled_) { 16273 this.buttonPressed_ = false; 16274 this.menu.unlockShowing(); 16275 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 16276 } 16277 }; 16278 16279 /** 16280 * Disable the `MenuButton`. Don't allow it to be clicked. 16281 */ 16282 16283 16284 MenuButton.prototype.disable = function disable() { 16285 this.unpressButton(); 16286 16287 this.enabled_ = false; 16288 this.addClass('vjs-disabled'); 16289 16290 this.menuButton_.disable(); 16291 }; 16292 16293 /** 16294 * Enable the `MenuButton`. Allow it to be clicked. 16295 */ 16296 16297 16298 MenuButton.prototype.enable = function enable() { 16299 this.enabled_ = true; 16300 this.removeClass('vjs-disabled'); 16301 16302 this.menuButton_.enable(); 16303 }; 16304 16305 return MenuButton; 16306 }(Component); 16307 16308 Component.registerComponent('MenuButton', MenuButton); 16309 16310 /** 16311 * @file track-button.js 16312 */ 16313 16314 /** 16315 * The base class for buttons that toggle specific track types (e.g. subtitles). 16316 * 16317 * @extends MenuButton 16318 */ 16319 16320 var TrackButton = function (_MenuButton) { 16321 inherits(TrackButton, _MenuButton); 16322 16323 /** 16324 * Creates an instance of this class. 16325 * 16326 * @param {Player} player 16327 * The `Player` that this class should be attached to. 16328 * 16329 * @param {Object} [options] 16330 * The key/value store of player options. 16331 */ 16332 function TrackButton(player, options) { 16333 classCallCheck(this, TrackButton); 16334 16335 var tracks = options.tracks; 16336 16337 var _this = possibleConstructorReturn(this, _MenuButton.call(this, player, options)); 16338 16339 if (_this.items.length <= 1) { 16340 _this.hide(); 16341 } 16342 16343 if (!tracks) { 16344 return possibleConstructorReturn(_this); 16345 } 16346 16347 var updateHandler = bind(_this, _this.update); 16348 16349 tracks.addEventListener('removetrack', updateHandler); 16350 tracks.addEventListener('addtrack', updateHandler); 16351 _this.player_.on('ready', updateHandler); 16352 16353 _this.player_.on('dispose', function () { 16354 tracks.removeEventListener('removetrack', updateHandler); 16355 tracks.removeEventListener('addtrack', updateHandler); 16356 }); 16357 return _this; 16358 } 16359 16360 return TrackButton; 16361 }(MenuButton); 16362 16363 Component.registerComponent('TrackButton', TrackButton); 16364 16365 /** 16366 * @file menu-item.js 16367 */ 16368 16369 /** 16370 * The component for a menu item. `<li>` 16371 * 16372 * @extends ClickableComponent 16373 */ 16374 16375 var MenuItem = function (_ClickableComponent) { 16376 inherits(MenuItem, _ClickableComponent); 16377 16378 /** 16379 * Creates an instance of the this class. 16380 * 16381 * @param {Player} player 16382 * The `Player` that this class should be attached to. 16383 * 16384 * @param {Object} [options={}] 16385 * The key/value store of player options. 16386 * 16387 */ 16388 function MenuItem(player, options) { 16389 classCallCheck(this, MenuItem); 16390 16391 var _this = possibleConstructorReturn(this, _ClickableComponent.call(this, player, options)); 16392 16393 _this.selectable = options.selectable; 16394 _this.isSelected_ = options.selected || false; 16395 _this.multiSelectable = options.multiSelectable; 16396 16397 _this.selected(_this.isSelected_); 16398 16399 if (_this.selectable) { 16400 if (_this.multiSelectable) { 16401 _this.el_.setAttribute('role', 'menuitemcheckbox'); 16402 } else { 16403 _this.el_.setAttribute('role', 'menuitemradio'); 16404 } 16405 } else { 16406 _this.el_.setAttribute('role', 'menuitem'); 16407 } 16408 return _this; 16409 } 16410 16411 /** 16412 * Create the `MenuItem's DOM element 16413 * 16414 * @param {string} [type=li] 16415 * Element's node type, not actually used, always set to `li`. 16416 * 16417 * @param {Object} [props={}] 16418 * An object of properties that should be set on the element 16419 * 16420 * @param {Object} [attrs={}] 16421 * An object of attributes that should be set on the element 16422 * 16423 * @return {Element} 16424 * The element that gets created. 16425 */ 16426 16427 16428 MenuItem.prototype.createEl = function createEl(type, props, attrs) { 16429 // The control is textual, not just an icon 16430 this.nonIconControl = true; 16431 16432 return _ClickableComponent.prototype.createEl.call(this, 'li', assign({ 16433 className: 'vjs-menu-item', 16434 innerHTML: '<span class="vjs-menu-item-text">' + this.localize(this.options_.label) + '</span>', 16435 tabIndex: -1 16436 }, props), attrs); 16437 }; 16438 16439 /** 16440 * Any click on a `MenuItem` puts it into the selected state. 16441 * See {@link ClickableComponent#handleClick} for instances where this is called. 16442 * 16443 * @param {EventTarget~Event} event 16444 * The `keydown`, `tap`, or `click` event that caused this function to be 16445 * called. 16446 * 16447 * @listens tap 16448 * @listens click 16449 */ 16450 16451 16452 MenuItem.prototype.handleClick = function handleClick(event) { 16453 this.selected(true); 16454 }; 16455 16456 /** 16457 * Set the state for this menu item as selected or not. 16458 * 16459 * @param {boolean} selected 16460 * if the menu item is selected or not 16461 */ 16462 16463 16464 MenuItem.prototype.selected = function selected(_selected) { 16465 if (this.selectable) { 16466 if (_selected) { 16467 this.addClass('vjs-selected'); 16468 this.el_.setAttribute('aria-checked', 'true'); 16469 // aria-checked isn't fully supported by browsers/screen readers, 16470 // so indicate selected state to screen reader in the control text. 16471 this.controlText(', selected'); 16472 this.isSelected_ = true; 16473 } else { 16474 this.removeClass('vjs-selected'); 16475 this.el_.setAttribute('aria-checked', 'false'); 16476 // Indicate un-selected state to screen reader 16477 this.controlText(''); 16478 this.isSelected_ = false; 16479 } 16480 } 16481 }; 16482 16483 return MenuItem; 16484 }(ClickableComponent); 16485 16486 Component.registerComponent('MenuItem', MenuItem); 16487 16488 /** 16489 * @file text-track-menu-item.js 16490 */ 16491 16492 /** 16493 * The specific menu item type for selecting a language within a text track kind 16494 * 16495 * @extends MenuItem 16496 */ 16497 16498 var TextTrackMenuItem = function (_MenuItem) { 16499 inherits(TextTrackMenuItem, _MenuItem); 16500 16501 /** 16502 * Creates an instance of this class. 16503 * 16504 * @param {Player} player 16505 * The `Player` that this class should be attached to. 16506 * 16507 * @param {Object} [options] 16508 * The key/value store of player options. 16509 */ 16510 function TextTrackMenuItem(player, options) { 16511 classCallCheck(this, TextTrackMenuItem); 16512 16513 var track = options.track; 16514 var tracks = player.textTracks(); 16515 16516 // Modify options for parent MenuItem class's init. 16517 options.label = track.label || track.language || 'Unknown'; 16518 options.selected = track.mode === 'showing'; 16519 16520 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 16521 16522 _this.track = track; 16523 var changeHandler = function changeHandler() { 16524 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 16525 args[_key] = arguments[_key]; 16526 } 16527 16528 _this.handleTracksChange.apply(_this, args); 16529 }; 16530 var selectedLanguageChangeHandler = function selectedLanguageChangeHandler() { 16531 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 16532 args[_key2] = arguments[_key2]; 16533 } 16534 16535 _this.handleSelectedLanguageChange.apply(_this, args); 16536 }; 16537 16538 player.on(['loadstart', 'texttrackchange'], changeHandler); 16539 tracks.addEventListener('change', changeHandler); 16540 tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 16541 _this.on('dispose', function () { 16542 player.off(['loadstart', 'texttrackchange'], changeHandler); 16543 tracks.removeEventListener('change', changeHandler); 16544 tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 16545 }); 16546 16547 // iOS7 doesn't dispatch change events to TextTrackLists when an 16548 // associated track's mode changes. Without something like 16549 // Object.observe() (also not present on iOS7), it's not 16550 // possible to detect changes to the mode attribute and polyfill 16551 // the change event. As a poor substitute, we manually dispatch 16552 // change events whenever the controls modify the mode. 16553 if (tracks.onchange === undefined) { 16554 var event = void 0; 16555 16556 _this.on(['tap', 'click'], function () { 16557 if (_typeof(window_1.Event) !== 'object') { 16558 // Android 2.3 throws an Illegal Constructor error for window.Event 16559 try { 16560 event = new window_1.Event('change'); 16561 } catch (err) { 16562 // continue regardless of error 16563 } 16564 } 16565 16566 if (!event) { 16567 event = document_1.createEvent('Event'); 16568 event.initEvent('change', true, true); 16569 } 16570 16571 tracks.dispatchEvent(event); 16572 }); 16573 } 16574 16575 // set the default state based on current tracks 16576 _this.handleTracksChange(); 16577 return _this; 16578 } 16579 16580 /** 16581 * This gets called when an `TextTrackMenuItem` is "clicked". See 16582 * {@link ClickableComponent} for more detailed information on what a click can be. 16583 * 16584 * @param {EventTarget~Event} event 16585 * The `keydown`, `tap`, or `click` event that caused this function to be 16586 * called. 16587 * 16588 * @listens tap 16589 * @listens click 16590 */ 16591 16592 16593 TextTrackMenuItem.prototype.handleClick = function handleClick(event) { 16594 var kind = this.track.kind; 16595 var kinds = this.track.kinds; 16596 var tracks = this.player_.textTracks(); 16597 16598 if (!kinds) { 16599 kinds = [kind]; 16600 } 16601 16602 _MenuItem.prototype.handleClick.call(this, event); 16603 16604 if (!tracks) { 16605 return; 16606 } 16607 16608 for (var i = 0; i < tracks.length; i++) { 16609 var track = tracks[i]; 16610 16611 if (track === this.track && kinds.indexOf(track.kind) > -1) { 16612 if (track.mode !== 'showing') { 16613 track.mode = 'showing'; 16614 } 16615 } else if (track.mode !== 'disabled') { 16616 track.mode = 'disabled'; 16617 } 16618 } 16619 }; 16620 16621 /** 16622 * Handle text track list change 16623 * 16624 * @param {EventTarget~Event} event 16625 * The `change` event that caused this function to be called. 16626 * 16627 * @listens TextTrackList#change 16628 */ 16629 16630 16631 TextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { 16632 var shouldBeSelected = this.track.mode === 'showing'; 16633 16634 // Prevent redundant selected() calls because they may cause 16635 // screen readers to read the appended control text unnecessarily 16636 if (shouldBeSelected !== this.isSelected_) { 16637 this.selected(shouldBeSelected); 16638 } 16639 }; 16640 16641 TextTrackMenuItem.prototype.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { 16642 if (this.track.mode === 'showing') { 16643 var selectedLanguage = this.player_.cache_.selectedLanguage; 16644 16645 // Don't replace the kind of track across the same language 16646 if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) { 16647 return; 16648 } 16649 16650 this.player_.cache_.selectedLanguage = { 16651 enabled: true, 16652 language: this.track.language, 16653 kind: this.track.kind 16654 }; 16655 } 16656 }; 16657 16658 TextTrackMenuItem.prototype.dispose = function dispose() { 16659 // remove reference to track object on dispose 16660 this.track = null; 16661 16662 _MenuItem.prototype.dispose.call(this); 16663 }; 16664 16665 return TextTrackMenuItem; 16666 }(MenuItem); 16667 16668 Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem); 16669 16670 /** 16671 * @file off-text-track-menu-item.js 16672 */ 16673 16674 /** 16675 * A special menu item for turning of a specific type of text track 16676 * 16677 * @extends TextTrackMenuItem 16678 */ 16679 16680 var OffTextTrackMenuItem = function (_TextTrackMenuItem) { 16681 inherits(OffTextTrackMenuItem, _TextTrackMenuItem); 16682 16683 /** 16684 * Creates an instance of this class. 16685 * 16686 * @param {Player} player 16687 * The `Player` that this class should be attached to. 16688 * 16689 * @param {Object} [options] 16690 * The key/value store of player options. 16691 */ 16692 function OffTextTrackMenuItem(player, options) { 16693 classCallCheck(this, OffTextTrackMenuItem); 16694 16695 // Create pseudo track info 16696 // Requires options['kind'] 16697 options.track = { 16698 player: player, 16699 kind: options.kind, 16700 kinds: options.kinds, 16701 default: false, 16702 mode: 'disabled' 16703 }; 16704 16705 if (!options.kinds) { 16706 options.kinds = [options.kind]; 16707 } 16708 16709 if (options.label) { 16710 options.track.label = options.label; 16711 } else { 16712 options.track.label = options.kinds.join(' and ') + ' off'; 16713 } 16714 16715 // MenuItem is selectable 16716 options.selectable = true; 16717 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 16718 options.multiSelectable = false; 16719 16720 return possibleConstructorReturn(this, _TextTrackMenuItem.call(this, player, options)); 16721 } 16722 16723 /** 16724 * Handle text track change 16725 * 16726 * @param {EventTarget~Event} event 16727 * The event that caused this function to run 16728 */ 16729 16730 16731 OffTextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { 16732 var tracks = this.player().textTracks(); 16733 var shouldBeSelected = true; 16734 16735 for (var i = 0, l = tracks.length; i < l; i++) { 16736 var track = tracks[i]; 16737 16738 if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') { 16739 shouldBeSelected = false; 16740 break; 16741 } 16742 } 16743 16744 // Prevent redundant selected() calls because they may cause 16745 // screen readers to read the appended control text unnecessarily 16746 if (shouldBeSelected !== this.isSelected_) { 16747 this.selected(shouldBeSelected); 16748 } 16749 }; 16750 16751 OffTextTrackMenuItem.prototype.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { 16752 var tracks = this.player().textTracks(); 16753 var allHidden = true; 16754 16755 for (var i = 0, l = tracks.length; i < l; i++) { 16756 var track = tracks[i]; 16757 16758 if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') { 16759 allHidden = false; 16760 break; 16761 } 16762 } 16763 16764 if (allHidden) { 16765 this.player_.cache_.selectedLanguage = { 16766 enabled: false 16767 }; 16768 } 16769 }; 16770 16771 return OffTextTrackMenuItem; 16772 }(TextTrackMenuItem); 16773 16774 Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); 16775 16776 /** 16777 * @file text-track-button.js 16778 */ 16779 16780 /** 16781 * The base class for buttons that toggle specific text track types (e.g. subtitles) 16782 * 16783 * @extends MenuButton 16784 */ 16785 16786 var TextTrackButton = function (_TrackButton) { 16787 inherits(TextTrackButton, _TrackButton); 16788 16789 /** 16790 * Creates an instance of this class. 16791 * 16792 * @param {Player} player 16793 * The `Player` that this class should be attached to. 16794 * 16795 * @param {Object} [options={}] 16796 * The key/value store of player options. 16797 */ 16798 function TextTrackButton(player) { 16799 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 16800 classCallCheck(this, TextTrackButton); 16801 16802 options.tracks = player.textTracks(); 16803 16804 return possibleConstructorReturn(this, _TrackButton.call(this, player, options)); 16805 } 16806 16807 /** 16808 * Create a menu item for each text track 16809 * 16810 * @param {TextTrackMenuItem[]} [items=[]] 16811 * Existing array of items to use during creation 16812 * 16813 * @return {TextTrackMenuItem[]} 16814 * Array of menu items that were created 16815 */ 16816 16817 16818 TextTrackButton.prototype.createItems = function createItems() { 16819 var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 16820 var TrackMenuItem = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : TextTrackMenuItem; 16821 16822 16823 // Label is an override for the [track] off label 16824 // USed to localise captions/subtitles 16825 var label = void 0; 16826 16827 if (this.label_) { 16828 label = this.label_ + ' off'; 16829 } 16830 // Add an OFF menu item to turn all tracks off 16831 items.push(new OffTextTrackMenuItem(this.player_, { 16832 kinds: this.kinds_, 16833 kind: this.kind_, 16834 label: label 16835 })); 16836 16837 this.hideThreshold_ += 1; 16838 16839 var tracks = this.player_.textTracks(); 16840 16841 if (!Array.isArray(this.kinds_)) { 16842 this.kinds_ = [this.kind_]; 16843 } 16844 16845 for (var i = 0; i < tracks.length; i++) { 16846 var track = tracks[i]; 16847 16848 // only add tracks that are of an appropriate kind and have a label 16849 if (this.kinds_.indexOf(track.kind) > -1) { 16850 16851 var item = new TrackMenuItem(this.player_, { 16852 track: track, 16853 // MenuItem is selectable 16854 selectable: true, 16855 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 16856 multiSelectable: false 16857 }); 16858 16859 item.addClass('vjs-' + track.kind + '-menu-item'); 16860 items.push(item); 16861 } 16862 } 16863 16864 return items; 16865 }; 16866 16867 return TextTrackButton; 16868 }(TrackButton); 16869 16870 Component.registerComponent('TextTrackButton', TextTrackButton); 16871 16872 /** 16873 * @file chapters-track-menu-item.js 16874 */ 16875 16876 /** 16877 * The chapter track menu item 16878 * 16879 * @extends MenuItem 16880 */ 16881 16882 var ChaptersTrackMenuItem = function (_MenuItem) { 16883 inherits(ChaptersTrackMenuItem, _MenuItem); 16884 16885 /** 16886 * Creates an instance of this class. 16887 * 16888 * @param {Player} player 16889 * The `Player` that this class should be attached to. 16890 * 16891 * @param {Object} [options] 16892 * The key/value store of player options. 16893 */ 16894 function ChaptersTrackMenuItem(player, options) { 16895 classCallCheck(this, ChaptersTrackMenuItem); 16896 16897 var track = options.track; 16898 var cue = options.cue; 16899 var currentTime = player.currentTime(); 16900 16901 // Modify options for parent MenuItem class's init. 16902 options.selectable = true; 16903 options.multiSelectable = false; 16904 options.label = cue.text; 16905 options.selected = cue.startTime <= currentTime && currentTime < cue.endTime; 16906 16907 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 16908 16909 _this.track = track; 16910 _this.cue = cue; 16911 track.addEventListener('cuechange', bind(_this, _this.update)); 16912 return _this; 16913 } 16914 16915 /** 16916 * This gets called when an `ChaptersTrackMenuItem` is "clicked". See 16917 * {@link ClickableComponent} for more detailed information on what a click can be. 16918 * 16919 * @param {EventTarget~Event} [event] 16920 * The `keydown`, `tap`, or `click` event that caused this function to be 16921 * called. 16922 * 16923 * @listens tap 16924 * @listens click 16925 */ 16926 16927 16928 ChaptersTrackMenuItem.prototype.handleClick = function handleClick(event) { 16929 _MenuItem.prototype.handleClick.call(this); 16930 this.player_.currentTime(this.cue.startTime); 16931 this.update(this.cue.startTime); 16932 }; 16933 16934 /** 16935 * Update chapter menu item 16936 * 16937 * @param {EventTarget~Event} [event] 16938 * The `cuechange` event that caused this function to run. 16939 * 16940 * @listens TextTrack#cuechange 16941 */ 16942 16943 16944 ChaptersTrackMenuItem.prototype.update = function update(event) { 16945 var cue = this.cue; 16946 var currentTime = this.player_.currentTime(); 16947 16948 // vjs.log(currentTime, cue.startTime); 16949 this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); 16950 }; 16951 16952 return ChaptersTrackMenuItem; 16953 }(MenuItem); 16954 16955 Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); 16956 16957 /** 16958 * @file chapters-button.js 16959 */ 16960 16961 /** 16962 * The button component for toggling and selecting chapters 16963 * Chapters act much differently than other text tracks 16964 * Cues are navigation vs. other tracks of alternative languages 16965 * 16966 * @extends TextTrackButton 16967 */ 16968 16969 var ChaptersButton = function (_TextTrackButton) { 16970 inherits(ChaptersButton, _TextTrackButton); 16971 16972 /** 16973 * Creates an instance of this class. 16974 * 16975 * @param {Player} player 16976 * The `Player` that this class should be attached to. 16977 * 16978 * @param {Object} [options] 16979 * The key/value store of player options. 16980 * 16981 * @param {Component~ReadyCallback} [ready] 16982 * The function to call when this function is ready. 16983 */ 16984 function ChaptersButton(player, options, ready) { 16985 classCallCheck(this, ChaptersButton); 16986 return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 16987 } 16988 16989 /** 16990 * Builds the default DOM `className`. 16991 * 16992 * @return {string} 16993 * The DOM `className` for this object. 16994 */ 16995 16996 16997 ChaptersButton.prototype.buildCSSClass = function buildCSSClass() { 16998 return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 16999 }; 17000 17001 ChaptersButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17002 return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17003 }; 17004 17005 /** 17006 * Update the menu based on the current state of its items. 17007 * 17008 * @param {EventTarget~Event} [event] 17009 * An event that triggered this function to run. 17010 * 17011 * @listens TextTrackList#addtrack 17012 * @listens TextTrackList#removetrack 17013 * @listens TextTrackList#change 17014 */ 17015 17016 17017 ChaptersButton.prototype.update = function update(event) { 17018 if (!this.track_ || event && (event.type === 'addtrack' || event.type === 'removetrack')) { 17019 this.setTrack(this.findChaptersTrack()); 17020 } 17021 _TextTrackButton.prototype.update.call(this); 17022 }; 17023 17024 /** 17025 * Set the currently selected track for the chapters button. 17026 * 17027 * @param {TextTrack} track 17028 * The new track to select. Nothing will change if this is the currently selected 17029 * track. 17030 */ 17031 17032 17033 ChaptersButton.prototype.setTrack = function setTrack(track) { 17034 if (this.track_ === track) { 17035 return; 17036 } 17037 17038 if (!this.updateHandler_) { 17039 this.updateHandler_ = this.update.bind(this); 17040 } 17041 17042 // here this.track_ refers to the old track instance 17043 if (this.track_) { 17044 var remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 17045 17046 if (remoteTextTrackEl) { 17047 remoteTextTrackEl.removeEventListener('load', this.updateHandler_); 17048 } 17049 17050 this.track_ = null; 17051 } 17052 17053 this.track_ = track; 17054 17055 // here this.track_ refers to the new track instance 17056 if (this.track_) { 17057 this.track_.mode = 'hidden'; 17058 17059 var _remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 17060 17061 if (_remoteTextTrackEl) { 17062 _remoteTextTrackEl.addEventListener('load', this.updateHandler_); 17063 } 17064 } 17065 }; 17066 17067 /** 17068 * Find the track object that is currently in use by this ChaptersButton 17069 * 17070 * @return {TextTrack|undefined} 17071 * The current track or undefined if none was found. 17072 */ 17073 17074 17075 ChaptersButton.prototype.findChaptersTrack = function findChaptersTrack() { 17076 var tracks = this.player_.textTracks() || []; 17077 17078 for (var i = tracks.length - 1; i >= 0; i--) { 17079 // We will always choose the last track as our chaptersTrack 17080 var track = tracks[i]; 17081 17082 if (track.kind === this.kind_) { 17083 return track; 17084 } 17085 } 17086 }; 17087 17088 /** 17089 * Get the caption for the ChaptersButton based on the track label. This will also 17090 * use the current tracks localized kind as a fallback if a label does not exist. 17091 * 17092 * @return {string} 17093 * The tracks current label or the localized track kind. 17094 */ 17095 17096 17097 ChaptersButton.prototype.getMenuCaption = function getMenuCaption() { 17098 if (this.track_ && this.track_.label) { 17099 return this.track_.label; 17100 } 17101 return this.localize(toTitleCase(this.kind_)); 17102 }; 17103 17104 /** 17105 * Create menu from chapter track 17106 * 17107 * @return {Menu} 17108 * New menu for the chapter buttons 17109 */ 17110 17111 17112 ChaptersButton.prototype.createMenu = function createMenu() { 17113 this.options_.title = this.getMenuCaption(); 17114 return _TextTrackButton.prototype.createMenu.call(this); 17115 }; 17116 17117 /** 17118 * Create a menu item for each text track 17119 * 17120 * @return {TextTrackMenuItem[]} 17121 * Array of menu items 17122 */ 17123 17124 17125 ChaptersButton.prototype.createItems = function createItems() { 17126 var items = []; 17127 17128 if (!this.track_) { 17129 return items; 17130 } 17131 17132 var cues = this.track_.cues; 17133 17134 if (!cues) { 17135 return items; 17136 } 17137 17138 for (var i = 0, l = cues.length; i < l; i++) { 17139 var cue = cues[i]; 17140 var mi = new ChaptersTrackMenuItem(this.player_, { track: this.track_, cue: cue }); 17141 17142 items.push(mi); 17143 } 17144 17145 return items; 17146 }; 17147 17148 return ChaptersButton; 17149 }(TextTrackButton); 17150 17151 /** 17152 * `kind` of TextTrack to look for to associate it with this menu. 17153 * 17154 * @type {string} 17155 * @private 17156 */ 17157 17158 17159 ChaptersButton.prototype.kind_ = 'chapters'; 17160 17161 /** 17162 * The text that should display over the `ChaptersButton`s controls. Added for localization. 17163 * 17164 * @type {string} 17165 * @private 17166 */ 17167 ChaptersButton.prototype.controlText_ = 'Chapters'; 17168 17169 Component.registerComponent('ChaptersButton', ChaptersButton); 17170 17171 /** 17172 * @file descriptions-button.js 17173 */ 17174 17175 /** 17176 * The button component for toggling and selecting descriptions 17177 * 17178 * @extends TextTrackButton 17179 */ 17180 17181 var DescriptionsButton = function (_TextTrackButton) { 17182 inherits(DescriptionsButton, _TextTrackButton); 17183 17184 /** 17185 * Creates an instance of this class. 17186 * 17187 * @param {Player} player 17188 * The `Player` that this class should be attached to. 17189 * 17190 * @param {Object} [options] 17191 * The key/value store of player options. 17192 * 17193 * @param {Component~ReadyCallback} [ready] 17194 * The function to call when this component is ready. 17195 */ 17196 function DescriptionsButton(player, options, ready) { 17197 classCallCheck(this, DescriptionsButton); 17198 17199 var _this = possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17200 17201 var tracks = player.textTracks(); 17202 var changeHandler = bind(_this, _this.handleTracksChange); 17203 17204 tracks.addEventListener('change', changeHandler); 17205 _this.on('dispose', function () { 17206 tracks.removeEventListener('change', changeHandler); 17207 }); 17208 return _this; 17209 } 17210 17211 /** 17212 * Handle text track change 17213 * 17214 * @param {EventTarget~Event} event 17215 * The event that caused this function to run 17216 * 17217 * @listens TextTrackList#change 17218 */ 17219 17220 17221 DescriptionsButton.prototype.handleTracksChange = function handleTracksChange(event) { 17222 var tracks = this.player().textTracks(); 17223 var disabled = false; 17224 17225 // Check whether a track of a different kind is showing 17226 for (var i = 0, l = tracks.length; i < l; i++) { 17227 var track = tracks[i]; 17228 17229 if (track.kind !== this.kind_ && track.mode === 'showing') { 17230 disabled = true; 17231 break; 17232 } 17233 } 17234 17235 // If another track is showing, disable this menu button 17236 if (disabled) { 17237 this.disable(); 17238 } else { 17239 this.enable(); 17240 } 17241 }; 17242 17243 /** 17244 * Builds the default DOM `className`. 17245 * 17246 * @return {string} 17247 * The DOM `className` for this object. 17248 */ 17249 17250 17251 DescriptionsButton.prototype.buildCSSClass = function buildCSSClass() { 17252 return 'vjs-descriptions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17253 }; 17254 17255 DescriptionsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17256 return 'vjs-descriptions-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17257 }; 17258 17259 return DescriptionsButton; 17260 }(TextTrackButton); 17261 17262 /** 17263 * `kind` of TextTrack to look for to associate it with this menu. 17264 * 17265 * @type {string} 17266 * @private 17267 */ 17268 17269 17270 DescriptionsButton.prototype.kind_ = 'descriptions'; 17271 17272 /** 17273 * The text that should display over the `DescriptionsButton`s controls. Added for localization. 17274 * 17275 * @type {string} 17276 * @private 17277 */ 17278 DescriptionsButton.prototype.controlText_ = 'Descriptions'; 17279 17280 Component.registerComponent('DescriptionsButton', DescriptionsButton); 17281 17282 /** 17283 * @file subtitles-button.js 17284 */ 17285 17286 /** 17287 * The button component for toggling and selecting subtitles 17288 * 17289 * @extends TextTrackButton 17290 */ 17291 17292 var SubtitlesButton = function (_TextTrackButton) { 17293 inherits(SubtitlesButton, _TextTrackButton); 17294 17295 /** 17296 * Creates an instance of this class. 17297 * 17298 * @param {Player} player 17299 * The `Player` that this class should be attached to. 17300 * 17301 * @param {Object} [options] 17302 * The key/value store of player options. 17303 * 17304 * @param {Component~ReadyCallback} [ready] 17305 * The function to call when this component is ready. 17306 */ 17307 function SubtitlesButton(player, options, ready) { 17308 classCallCheck(this, SubtitlesButton); 17309 return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17310 } 17311 17312 /** 17313 * Builds the default DOM `className`. 17314 * 17315 * @return {string} 17316 * The DOM `className` for this object. 17317 */ 17318 17319 17320 SubtitlesButton.prototype.buildCSSClass = function buildCSSClass() { 17321 return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17322 }; 17323 17324 SubtitlesButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17325 return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17326 }; 17327 17328 return SubtitlesButton; 17329 }(TextTrackButton); 17330 17331 /** 17332 * `kind` of TextTrack to look for to associate it with this menu. 17333 * 17334 * @type {string} 17335 * @private 17336 */ 17337 17338 17339 SubtitlesButton.prototype.kind_ = 'subtitles'; 17340 17341 /** 17342 * The text that should display over the `SubtitlesButton`s controls. Added for localization. 17343 * 17344 * @type {string} 17345 * @private 17346 */ 17347 SubtitlesButton.prototype.controlText_ = 'Subtitles'; 17348 17349 Component.registerComponent('SubtitlesButton', SubtitlesButton); 17350 17351 /** 17352 * @file caption-settings-menu-item.js 17353 */ 17354 17355 /** 17356 * The menu item for caption track settings menu 17357 * 17358 * @extends TextTrackMenuItem 17359 */ 17360 17361 var CaptionSettingsMenuItem = function (_TextTrackMenuItem) { 17362 inherits(CaptionSettingsMenuItem, _TextTrackMenuItem); 17363 17364 /** 17365 * Creates an instance of this class. 17366 * 17367 * @param {Player} player 17368 * The `Player` that this class should be attached to. 17369 * 17370 * @param {Object} [options] 17371 * The key/value store of player options. 17372 */ 17373 function CaptionSettingsMenuItem(player, options) { 17374 classCallCheck(this, CaptionSettingsMenuItem); 17375 17376 options.track = { 17377 player: player, 17378 kind: options.kind, 17379 label: options.kind + ' settings', 17380 selectable: false, 17381 default: false, 17382 mode: 'disabled' 17383 }; 17384 17385 // CaptionSettingsMenuItem has no concept of 'selected' 17386 options.selectable = false; 17387 17388 options.name = 'CaptionSettingsMenuItem'; 17389 17390 var _this = possibleConstructorReturn(this, _TextTrackMenuItem.call(this, player, options)); 17391 17392 _this.addClass('vjs-texttrack-settings'); 17393 _this.controlText(', opens ' + options.kind + ' settings dialog'); 17394 return _this; 17395 } 17396 17397 /** 17398 * This gets called when an `CaptionSettingsMenuItem` is "clicked". See 17399 * {@link ClickableComponent} for more detailed information on what a click can be. 17400 * 17401 * @param {EventTarget~Event} [event] 17402 * The `keydown`, `tap`, or `click` event that caused this function to be 17403 * called. 17404 * 17405 * @listens tap 17406 * @listens click 17407 */ 17408 17409 17410 CaptionSettingsMenuItem.prototype.handleClick = function handleClick(event) { 17411 this.player().getChild('textTrackSettings').open(); 17412 }; 17413 17414 return CaptionSettingsMenuItem; 17415 }(TextTrackMenuItem); 17416 17417 Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); 17418 17419 /** 17420 * @file captions-button.js 17421 */ 17422 17423 /** 17424 * The button component for toggling and selecting captions 17425 * 17426 * @extends TextTrackButton 17427 */ 17428 17429 var CaptionsButton = function (_TextTrackButton) { 17430 inherits(CaptionsButton, _TextTrackButton); 17431 17432 /** 17433 * Creates an instance of this class. 17434 * 17435 * @param {Player} player 17436 * The `Player` that this class should be attached to. 17437 * 17438 * @param {Object} [options] 17439 * The key/value store of player options. 17440 * 17441 * @param {Component~ReadyCallback} [ready] 17442 * The function to call when this component is ready. 17443 */ 17444 function CaptionsButton(player, options, ready) { 17445 classCallCheck(this, CaptionsButton); 17446 return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17447 } 17448 17449 /** 17450 * Builds the default DOM `className`. 17451 * 17452 * @return {string} 17453 * The DOM `className` for this object. 17454 */ 17455 17456 17457 CaptionsButton.prototype.buildCSSClass = function buildCSSClass() { 17458 return 'vjs-captions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17459 }; 17460 17461 CaptionsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17462 return 'vjs-captions-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17463 }; 17464 17465 /** 17466 * Create caption menu items 17467 * 17468 * @return {CaptionSettingsMenuItem[]} 17469 * The array of current menu items. 17470 */ 17471 17472 17473 CaptionsButton.prototype.createItems = function createItems() { 17474 var items = []; 17475 17476 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 17477 items.push(new CaptionSettingsMenuItem(this.player_, { kind: this.kind_ })); 17478 17479 this.hideThreshold_ += 1; 17480 } 17481 17482 return _TextTrackButton.prototype.createItems.call(this, items); 17483 }; 17484 17485 return CaptionsButton; 17486 }(TextTrackButton); 17487 17488 /** 17489 * `kind` of TextTrack to look for to associate it with this menu. 17490 * 17491 * @type {string} 17492 * @private 17493 */ 17494 17495 17496 CaptionsButton.prototype.kind_ = 'captions'; 17497 17498 /** 17499 * The text that should display over the `CaptionsButton`s controls. Added for localization. 17500 * 17501 * @type {string} 17502 * @private 17503 */ 17504 CaptionsButton.prototype.controlText_ = 'Captions'; 17505 17506 Component.registerComponent('CaptionsButton', CaptionsButton); 17507 17508 /** 17509 * @file subs-caps-menu-item.js 17510 */ 17511 17512 /** 17513 * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles 17514 * in the SubsCapsMenu. 17515 * 17516 * @extends TextTrackMenuItem 17517 */ 17518 17519 var SubsCapsMenuItem = function (_TextTrackMenuItem) { 17520 inherits(SubsCapsMenuItem, _TextTrackMenuItem); 17521 17522 function SubsCapsMenuItem() { 17523 classCallCheck(this, SubsCapsMenuItem); 17524 return possibleConstructorReturn(this, _TextTrackMenuItem.apply(this, arguments)); 17525 } 17526 17527 SubsCapsMenuItem.prototype.createEl = function createEl(type, props, attrs) { 17528 var innerHTML = '<span class="vjs-menu-item-text">' + this.localize(this.options_.label); 17529 17530 if (this.options_.track.kind === 'captions') { 17531 innerHTML += '\n <span aria-hidden="true" class="vjs-icon-placeholder"></span>\n <span class="vjs-control-text"> ' + this.localize('Captions') + '</span>\n '; 17532 } 17533 17534 innerHTML += '</span>'; 17535 17536 var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({ 17537 innerHTML: innerHTML 17538 }, props), attrs); 17539 17540 return el; 17541 }; 17542 17543 return SubsCapsMenuItem; 17544 }(TextTrackMenuItem); 17545 17546 Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); 17547 17548 /** 17549 * @file sub-caps-button.js 17550 */ 17551 /** 17552 * The button component for toggling and selecting captions and/or subtitles 17553 * 17554 * @extends TextTrackButton 17555 */ 17556 17557 var SubsCapsButton = function (_TextTrackButton) { 17558 inherits(SubsCapsButton, _TextTrackButton); 17559 17560 function SubsCapsButton(player) { 17561 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 17562 classCallCheck(this, SubsCapsButton); 17563 17564 // Although North America uses "captions" in most cases for 17565 // "captions and subtitles" other locales use "subtitles" 17566 var _this = possibleConstructorReturn(this, _TextTrackButton.call(this, player, options)); 17567 17568 _this.label_ = 'subtitles'; 17569 if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(_this.player_.language_) > -1) { 17570 _this.label_ = 'captions'; 17571 } 17572 _this.menuButton_.controlText(toTitleCase(_this.label_)); 17573 return _this; 17574 } 17575 17576 /** 17577 * Builds the default DOM `className`. 17578 * 17579 * @return {string} 17580 * The DOM `className` for this object. 17581 */ 17582 17583 17584 SubsCapsButton.prototype.buildCSSClass = function buildCSSClass() { 17585 return 'vjs-subs-caps-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17586 }; 17587 17588 SubsCapsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17589 return 'vjs-subs-caps-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17590 }; 17591 17592 /** 17593 * Create caption/subtitles menu items 17594 * 17595 * @return {CaptionSettingsMenuItem[]} 17596 * The array of current menu items. 17597 */ 17598 17599 17600 SubsCapsButton.prototype.createItems = function createItems() { 17601 var items = []; 17602 17603 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 17604 items.push(new CaptionSettingsMenuItem(this.player_, { kind: this.label_ })); 17605 17606 this.hideThreshold_ += 1; 17607 } 17608 17609 items = _TextTrackButton.prototype.createItems.call(this, items, SubsCapsMenuItem); 17610 return items; 17611 }; 17612 17613 return SubsCapsButton; 17614 }(TextTrackButton); 17615 17616 /** 17617 * `kind`s of TextTrack to look for to associate it with this menu. 17618 * 17619 * @type {array} 17620 * @private 17621 */ 17622 17623 17624 SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles']; 17625 17626 /** 17627 * The text that should display over the `SubsCapsButton`s controls. 17628 * 17629 * 17630 * @type {string} 17631 * @private 17632 */ 17633 SubsCapsButton.prototype.controlText_ = 'Subtitles'; 17634 17635 Component.registerComponent('SubsCapsButton', SubsCapsButton); 17636 17637 /** 17638 * @file audio-track-menu-item.js 17639 */ 17640 17641 /** 17642 * An {@link AudioTrack} {@link MenuItem} 17643 * 17644 * @extends MenuItem 17645 */ 17646 17647 var AudioTrackMenuItem = function (_MenuItem) { 17648 inherits(AudioTrackMenuItem, _MenuItem); 17649 17650 /** 17651 * Creates an instance of this class. 17652 * 17653 * @param {Player} player 17654 * The `Player` that this class should be attached to. 17655 * 17656 * @param {Object} [options] 17657 * The key/value store of player options. 17658 */ 17659 function AudioTrackMenuItem(player, options) { 17660 classCallCheck(this, AudioTrackMenuItem); 17661 17662 var track = options.track; 17663 var tracks = player.audioTracks(); 17664 17665 // Modify options for parent MenuItem class's init. 17666 options.label = track.label || track.language || 'Unknown'; 17667 options.selected = track.enabled; 17668 17669 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 17670 17671 _this.track = track; 17672 17673 var changeHandler = function changeHandler() { 17674 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 17675 args[_key] = arguments[_key]; 17676 } 17677 17678 _this.handleTracksChange.apply(_this, args); 17679 }; 17680 17681 tracks.addEventListener('change', changeHandler); 17682 _this.on('dispose', function () { 17683 tracks.removeEventListener('change', changeHandler); 17684 }); 17685 return _this; 17686 } 17687 17688 /** 17689 * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent} 17690 * for more detailed information on what a click can be. 17691 * 17692 * @param {EventTarget~Event} [event] 17693 * The `keydown`, `tap`, or `click` event that caused this function to be 17694 * called. 17695 * 17696 * @listens tap 17697 * @listens click 17698 */ 17699 17700 17701 AudioTrackMenuItem.prototype.handleClick = function handleClick(event) { 17702 var tracks = this.player_.audioTracks(); 17703 17704 _MenuItem.prototype.handleClick.call(this, event); 17705 17706 for (var i = 0; i < tracks.length; i++) { 17707 var track = tracks[i]; 17708 17709 track.enabled = track === this.track; 17710 } 17711 }; 17712 17713 /** 17714 * Handle any {@link AudioTrack} change. 17715 * 17716 * @param {EventTarget~Event} [event] 17717 * The {@link AudioTrackList#change} event that caused this to run. 17718 * 17719 * @listens AudioTrackList#change 17720 */ 17721 17722 17723 AudioTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { 17724 this.selected(this.track.enabled); 17725 }; 17726 17727 return AudioTrackMenuItem; 17728 }(MenuItem); 17729 17730 Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem); 17731 17732 /** 17733 * @file audio-track-button.js 17734 */ 17735 17736 /** 17737 * The base class for buttons that toggle specific {@link AudioTrack} types. 17738 * 17739 * @extends TrackButton 17740 */ 17741 17742 var AudioTrackButton = function (_TrackButton) { 17743 inherits(AudioTrackButton, _TrackButton); 17744 17745 /** 17746 * Creates an instance of this class. 17747 * 17748 * @param {Player} player 17749 * The `Player` that this class should be attached to. 17750 * 17751 * @param {Object} [options={}] 17752 * The key/value store of player options. 17753 */ 17754 function AudioTrackButton(player) { 17755 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 17756 classCallCheck(this, AudioTrackButton); 17757 17758 options.tracks = player.audioTracks(); 17759 17760 return possibleConstructorReturn(this, _TrackButton.call(this, player, options)); 17761 } 17762 17763 /** 17764 * Builds the default DOM `className`. 17765 * 17766 * @return {string} 17767 * The DOM `className` for this object. 17768 */ 17769 17770 17771 AudioTrackButton.prototype.buildCSSClass = function buildCSSClass() { 17772 return 'vjs-audio-button ' + _TrackButton.prototype.buildCSSClass.call(this); 17773 }; 17774 17775 AudioTrackButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17776 return 'vjs-audio-button ' + _TrackButton.prototype.buildWrapperCSSClass.call(this); 17777 }; 17778 17779 /** 17780 * Create a menu item for each audio track 17781 * 17782 * @param {AudioTrackMenuItem[]} [items=[]] 17783 * An array of existing menu items to use. 17784 * 17785 * @return {AudioTrackMenuItem[]} 17786 * An array of menu items 17787 */ 17788 17789 17790 AudioTrackButton.prototype.createItems = function createItems() { 17791 var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 17792 17793 // if there's only one audio track, there no point in showing it 17794 this.hideThreshold_ = 1; 17795 17796 var tracks = this.player_.audioTracks(); 17797 17798 for (var i = 0; i < tracks.length; i++) { 17799 var track = tracks[i]; 17800 17801 items.push(new AudioTrackMenuItem(this.player_, { 17802 track: track, 17803 // MenuItem is selectable 17804 selectable: true, 17805 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 17806 multiSelectable: false 17807 })); 17808 } 17809 17810 return items; 17811 }; 17812 17813 return AudioTrackButton; 17814 }(TrackButton); 17815 17816 /** 17817 * The text that should display over the `AudioTrackButton`s controls. Added for localization. 17818 * 17819 * @type {string} 17820 * @private 17821 */ 17822 17823 17824 AudioTrackButton.prototype.controlText_ = 'Audio Track'; 17825 Component.registerComponent('AudioTrackButton', AudioTrackButton); 17826 17827 /** 17828 * @file playback-rate-menu-item.js 17829 */ 17830 17831 /** 17832 * The specific menu item type for selecting a playback rate. 17833 * 17834 * @extends MenuItem 17835 */ 17836 17837 var PlaybackRateMenuItem = function (_MenuItem) { 17838 inherits(PlaybackRateMenuItem, _MenuItem); 17839 17840 /** 17841 * Creates an instance of this class. 17842 * 17843 * @param {Player} player 17844 * The `Player` that this class should be attached to. 17845 * 17846 * @param {Object} [options] 17847 * The key/value store of player options. 17848 */ 17849 function PlaybackRateMenuItem(player, options) { 17850 classCallCheck(this, PlaybackRateMenuItem); 17851 17852 var label = options.rate; 17853 var rate = parseFloat(label, 10); 17854 17855 // Modify options for parent MenuItem class's init. 17856 options.label = label; 17857 options.selected = rate === 1; 17858 options.selectable = true; 17859 options.multiSelectable = false; 17860 17861 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 17862 17863 _this.label = label; 17864 _this.rate = rate; 17865 17866 _this.on(player, 'ratechange', _this.update); 17867 return _this; 17868 } 17869 17870 /** 17871 * This gets called when an `PlaybackRateMenuItem` is "clicked". See 17872 * {@link ClickableComponent} for more detailed information on what a click can be. 17873 * 17874 * @param {EventTarget~Event} [event] 17875 * The `keydown`, `tap`, or `click` event that caused this function to be 17876 * called. 17877 * 17878 * @listens tap 17879 * @listens click 17880 */ 17881 17882 17883 PlaybackRateMenuItem.prototype.handleClick = function handleClick(event) { 17884 _MenuItem.prototype.handleClick.call(this); 17885 this.player().playbackRate(this.rate); 17886 }; 17887 17888 /** 17889 * Update the PlaybackRateMenuItem when the playbackrate changes. 17890 * 17891 * @param {EventTarget~Event} [event] 17892 * The `ratechange` event that caused this function to run. 17893 * 17894 * @listens Player#ratechange 17895 */ 17896 17897 17898 PlaybackRateMenuItem.prototype.update = function update(event) { 17899 this.selected(this.player().playbackRate() === this.rate); 17900 }; 17901 17902 return PlaybackRateMenuItem; 17903 }(MenuItem); 17904 17905 /** 17906 * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization. 17907 * 17908 * @type {string} 17909 * @private 17910 */ 17911 17912 17913 PlaybackRateMenuItem.prototype.contentElType = 'button'; 17914 17915 Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); 17916 17917 /** 17918 * @file playback-rate-menu-button.js 17919 */ 17920 17921 /** 17922 * The component for controlling the playback rate. 17923 * 17924 * @extends MenuButton 17925 */ 17926 17927 var PlaybackRateMenuButton = function (_MenuButton) { 17928 inherits(PlaybackRateMenuButton, _MenuButton); 17929 17930 /** 17931 * Creates an instance of this class. 17932 * 17933 * @param {Player} player 17934 * The `Player` that this class should be attached to. 17935 * 17936 * @param {Object} [options] 17937 * The key/value store of player options. 17938 */ 17939 function PlaybackRateMenuButton(player, options) { 17940 classCallCheck(this, PlaybackRateMenuButton); 17941 17942 var _this = possibleConstructorReturn(this, _MenuButton.call(this, player, options)); 17943 17944 _this.updateVisibility(); 17945 _this.updateLabel(); 17946 17947 _this.on(player, 'loadstart', _this.updateVisibility); 17948 _this.on(player, 'ratechange', _this.updateLabel); 17949 return _this; 17950 } 17951 17952 /** 17953 * Create the `Component`'s DOM element 17954 * 17955 * @return {Element} 17956 * The element that was created. 17957 */ 17958 17959 17960 PlaybackRateMenuButton.prototype.createEl = function createEl$$1() { 17961 var el = _MenuButton.prototype.createEl.call(this); 17962 17963 this.labelEl_ = createEl('div', { 17964 className: 'vjs-playback-rate-value', 17965 innerHTML: '1x' 17966 }); 17967 17968 el.appendChild(this.labelEl_); 17969 17970 return el; 17971 }; 17972 17973 PlaybackRateMenuButton.prototype.dispose = function dispose() { 17974 this.labelEl_ = null; 17975 17976 _MenuButton.prototype.dispose.call(this); 17977 }; 17978 17979 /** 17980 * Builds the default DOM `className`. 17981 * 17982 * @return {string} 17983 * The DOM `className` for this object. 17984 */ 17985 17986 17987 PlaybackRateMenuButton.prototype.buildCSSClass = function buildCSSClass() { 17988 return 'vjs-playback-rate ' + _MenuButton.prototype.buildCSSClass.call(this); 17989 }; 17990 17991 PlaybackRateMenuButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17992 return 'vjs-playback-rate ' + _MenuButton.prototype.buildWrapperCSSClass.call(this); 17993 }; 17994 17995 /** 17996 * Create the playback rate menu 17997 * 17998 * @return {Menu} 17999 * Menu object populated with {@link PlaybackRateMenuItem}s 18000 */ 18001 18002 18003 PlaybackRateMenuButton.prototype.createMenu = function createMenu() { 18004 var menu = new Menu(this.player()); 18005 var rates = this.playbackRates(); 18006 18007 if (rates) { 18008 for (var i = rates.length - 1; i >= 0; i--) { 18009 menu.addChild(new PlaybackRateMenuItem(this.player(), { rate: rates[i] + 'x' })); 18010 } 18011 } 18012 18013 return menu; 18014 }; 18015 18016 /** 18017 * Updates ARIA accessibility attributes 18018 */ 18019 18020 18021 PlaybackRateMenuButton.prototype.updateARIAAttributes = function updateARIAAttributes() { 18022 // Current playback rate 18023 this.el().setAttribute('aria-valuenow', this.player().playbackRate()); 18024 }; 18025 18026 /** 18027 * This gets called when an `PlaybackRateMenuButton` is "clicked". See 18028 * {@link ClickableComponent} for more detailed information on what a click can be. 18029 * 18030 * @param {EventTarget~Event} [event] 18031 * The `keydown`, `tap`, or `click` event that caused this function to be 18032 * called. 18033 * 18034 * @listens tap 18035 * @listens click 18036 */ 18037 18038 18039 PlaybackRateMenuButton.prototype.handleClick = function handleClick(event) { 18040 // select next rate option 18041 var currentRate = this.player().playbackRate(); 18042 var rates = this.playbackRates(); 18043 18044 // this will select first one if the last one currently selected 18045 var newRate = rates[0]; 18046 18047 for (var i = 0; i < rates.length; i++) { 18048 if (rates[i] > currentRate) { 18049 newRate = rates[i]; 18050 break; 18051 } 18052 } 18053 this.player().playbackRate(newRate); 18054 }; 18055 18056 /** 18057 * Get possible playback rates 18058 * 18059 * @return {Array} 18060 * All possible playback rates 18061 */ 18062 18063 18064 PlaybackRateMenuButton.prototype.playbackRates = function playbackRates() { 18065 return this.options_.playbackRates || this.options_.playerOptions && this.options_.playerOptions.playbackRates; 18066 }; 18067 18068 /** 18069 * Get whether playback rates is supported by the tech 18070 * and an array of playback rates exists 18071 * 18072 * @return {boolean} 18073 * Whether changing playback rate is supported 18074 */ 18075 18076 18077 PlaybackRateMenuButton.prototype.playbackRateSupported = function playbackRateSupported() { 18078 return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0; 18079 }; 18080 18081 /** 18082 * Hide playback rate controls when they're no playback rate options to select 18083 * 18084 * @param {EventTarget~Event} [event] 18085 * The event that caused this function to run. 18086 * 18087 * @listens Player#loadstart 18088 */ 18089 18090 18091 PlaybackRateMenuButton.prototype.updateVisibility = function updateVisibility(event) { 18092 if (this.playbackRateSupported()) { 18093 this.removeClass('vjs-hidden'); 18094 } else { 18095 this.addClass('vjs-hidden'); 18096 } 18097 }; 18098 18099 /** 18100 * Update button label when rate changed 18101 * 18102 * @param {EventTarget~Event} [event] 18103 * The event that caused this function to run. 18104 * 18105 * @listens Player#ratechange 18106 */ 18107 18108 18109 PlaybackRateMenuButton.prototype.updateLabel = function updateLabel(event) { 18110 if (this.playbackRateSupported()) { 18111 this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; 18112 } 18113 }; 18114 18115 return PlaybackRateMenuButton; 18116 }(MenuButton); 18117 18118 /** 18119 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 18120 * 18121 * @type {string} 18122 * @private 18123 */ 18124 18125 18126 PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate'; 18127 18128 Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); 18129 18130 /** 18131 * @file spacer.js 18132 */ 18133 18134 /** 18135 * Just an empty spacer element that can be used as an append point for plugins, etc. 18136 * Also can be used to create space between elements when necessary. 18137 * 18138 * @extends Component 18139 */ 18140 18141 var Spacer = function (_Component) { 18142 inherits(Spacer, _Component); 18143 18144 function Spacer() { 18145 classCallCheck(this, Spacer); 18146 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 18147 } 18148 18149 /** 18150 * Builds the default DOM `className`. 18151 * 18152 * @return {string} 18153 * The DOM `className` for this object. 18154 */ 18155 Spacer.prototype.buildCSSClass = function buildCSSClass() { 18156 return 'vjs-spacer ' + _Component.prototype.buildCSSClass.call(this); 18157 }; 18158 18159 /** 18160 * Create the `Component`'s DOM element 18161 * 18162 * @return {Element} 18163 * The element that was created. 18164 */ 18165 18166 18167 Spacer.prototype.createEl = function createEl() { 18168 return _Component.prototype.createEl.call(this, 'div', { 18169 className: this.buildCSSClass() 18170 }); 18171 }; 18172 18173 return Spacer; 18174 }(Component); 18175 18176 Component.registerComponent('Spacer', Spacer); 18177 18178 /** 18179 * @file custom-control-spacer.js 18180 */ 18181 18182 /** 18183 * Spacer specifically meant to be used as an insertion point for new plugins, etc. 18184 * 18185 * @extends Spacer 18186 */ 18187 18188 var CustomControlSpacer = function (_Spacer) { 18189 inherits(CustomControlSpacer, _Spacer); 18190 18191 function CustomControlSpacer() { 18192 classCallCheck(this, CustomControlSpacer); 18193 return possibleConstructorReturn(this, _Spacer.apply(this, arguments)); 18194 } 18195 18196 /** 18197 * Builds the default DOM `className`. 18198 * 18199 * @return {string} 18200 * The DOM `className` for this object. 18201 */ 18202 CustomControlSpacer.prototype.buildCSSClass = function buildCSSClass() { 18203 return 'vjs-custom-control-spacer ' + _Spacer.prototype.buildCSSClass.call(this); 18204 }; 18205 18206 /** 18207 * Create the `Component`'s DOM element 18208 * 18209 * @return {Element} 18210 * The element that was created. 18211 */ 18212 18213 18214 CustomControlSpacer.prototype.createEl = function createEl() { 18215 var el = _Spacer.prototype.createEl.call(this, { 18216 className: this.buildCSSClass() 18217 }); 18218 18219 // No-flex/table-cell mode requires there be some content 18220 // in the cell to fill the remaining space of the table. 18221 el.innerHTML = '\xA0'; 18222 return el; 18223 }; 18224 18225 return CustomControlSpacer; 18226 }(Spacer); 18227 18228 Component.registerComponent('CustomControlSpacer', CustomControlSpacer); 18229 18230 /** 18231 * @file control-bar.js 18232 */ 18233 18234 /** 18235 * Container of main controls. 18236 * 18237 * @extends Component 18238 */ 18239 18240 var ControlBar = function (_Component) { 18241 inherits(ControlBar, _Component); 18242 18243 function ControlBar() { 18244 classCallCheck(this, ControlBar); 18245 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 18246 } 18247 18248 /** 18249 * Create the `Component`'s DOM element 18250 * 18251 * @return {Element} 18252 * The element that was created. 18253 */ 18254 ControlBar.prototype.createEl = function createEl() { 18255 return _Component.prototype.createEl.call(this, 'div', { 18256 className: 'vjs-control-bar', 18257 dir: 'ltr' 18258 }); 18259 }; 18260 18261 return ControlBar; 18262 }(Component); 18263 18264 /** 18265 * Default options for `ControlBar` 18266 * 18267 * @type {Object} 18268 * @private 18269 */ 18270 18271 18272 ControlBar.prototype.options_ = { 18273 children: ['playToggle', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'fullscreenToggle'] 18274 }; 18275 18276 Component.registerComponent('ControlBar', ControlBar); 18277 18278 /** 18279 * @file error-display.js 18280 */ 18281 18282 /** 18283 * A display that indicates an error has occurred. This means that the video 18284 * is unplayable. 18285 * 18286 * @extends ModalDialog 18287 */ 18288 18289 var ErrorDisplay = function (_ModalDialog) { 18290 inherits(ErrorDisplay, _ModalDialog); 18291 18292 /** 18293 * Creates an instance of this class. 18294 * 18295 * @param {Player} player 18296 * The `Player` that this class should be attached to. 18297 * 18298 * @param {Object} [options] 18299 * The key/value store of player options. 18300 */ 18301 function ErrorDisplay(player, options) { 18302 classCallCheck(this, ErrorDisplay); 18303 18304 var _this = possibleConstructorReturn(this, _ModalDialog.call(this, player, options)); 18305 18306 _this.on(player, 'error', _this.open); 18307 return _this; 18308 } 18309 18310 /** 18311 * Builds the default DOM `className`. 18312 * 18313 * @return {string} 18314 * The DOM `className` for this object. 18315 * 18316 * @deprecated Since version 5. 18317 */ 18318 18319 18320 ErrorDisplay.prototype.buildCSSClass = function buildCSSClass() { 18321 return 'vjs-error-display ' + _ModalDialog.prototype.buildCSSClass.call(this); 18322 }; 18323 18324 /** 18325 * Gets the localized error message based on the `Player`s error. 18326 * 18327 * @return {string} 18328 * The `Player`s error message localized or an empty string. 18329 */ 18330 18331 18332 ErrorDisplay.prototype.content = function content() { 18333 var error = this.player().error(); 18334 18335 return error ? this.localize(error.message) : ''; 18336 }; 18337 18338 return ErrorDisplay; 18339 }(ModalDialog); 18340 18341 /** 18342 * The default options for an `ErrorDisplay`. 18343 * 18344 * @private 18345 */ 18346 18347 18348 ErrorDisplay.prototype.options_ = mergeOptions(ModalDialog.prototype.options_, { 18349 pauseOnOpen: false, 18350 fillAlways: true, 18351 temporary: false, 18352 uncloseable: true 18353 }); 18354 18355 Component.registerComponent('ErrorDisplay', ErrorDisplay); 18356 18357 /** 18358 * @file text-track-settings.js 18359 */ 18360 18361 var LOCAL_STORAGE_KEY = 'vjs-text-track-settings'; 18362 18363 var COLOR_BLACK = ['#000', 'Black']; 18364 var COLOR_BLUE = ['#00F', 'Blue']; 18365 var COLOR_CYAN = ['#0FF', 'Cyan']; 18366 var COLOR_GREEN = ['#0F0', 'Green']; 18367 var COLOR_MAGENTA = ['#F0F', 'Magenta']; 18368 var COLOR_RED = ['#F00', 'Red']; 18369 var COLOR_WHITE = ['#FFF', 'White']; 18370 var COLOR_YELLOW = ['#FF0', 'Yellow']; 18371 18372 var OPACITY_OPAQUE = ['1', 'Opaque']; 18373 var OPACITY_SEMI = ['0.5', 'Semi-Transparent']; 18374 var OPACITY_TRANS = ['0', 'Transparent']; 18375 18376 // Configuration for the various <select> elements in the DOM of this component. 18377 // 18378 // Possible keys include: 18379 // 18380 // `default`: 18381 // The default option index. Only needs to be provided if not zero. 18382 // `parser`: 18383 // A function which is used to parse the value from the selected option in 18384 // a customized way. 18385 // `selector`: 18386 // The selector used to find the associated <select> element. 18387 var selectConfigs = { 18388 backgroundColor: { 18389 selector: '.vjs-bg-color > select', 18390 id: 'captions-background-color-%s', 18391 label: 'Color', 18392 options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 18393 }, 18394 18395 backgroundOpacity: { 18396 selector: '.vjs-bg-opacity > select', 18397 id: 'captions-background-opacity-%s', 18398 label: 'Transparency', 18399 options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS] 18400 }, 18401 18402 color: { 18403 selector: '.vjs-fg-color > select', 18404 id: 'captions-foreground-color-%s', 18405 label: 'Color', 18406 options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 18407 }, 18408 18409 edgeStyle: { 18410 selector: '.vjs-edge-style > select', 18411 id: '%s', 18412 label: 'Text Edge Style', 18413 options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Dropshadow']] 18414 }, 18415 18416 fontFamily: { 18417 selector: '.vjs-font-family > select', 18418 id: 'captions-font-family-%s', 18419 label: 'Font Family', 18420 options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']] 18421 }, 18422 18423 fontPercent: { 18424 selector: '.vjs-font-percent > select', 18425 id: 'captions-font-size-%s', 18426 label: 'Font Size', 18427 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%']], 18428 default: 2, 18429 parser: function parser(v) { 18430 return v === '1.00' ? null : Number(v); 18431 } 18432 }, 18433 18434 textOpacity: { 18435 selector: '.vjs-text-opacity > select', 18436 id: 'captions-foreground-opacity-%s', 18437 label: 'Transparency', 18438 options: [OPACITY_OPAQUE, OPACITY_SEMI] 18439 }, 18440 18441 // Options for this object are defined below. 18442 windowColor: { 18443 selector: '.vjs-window-color > select', 18444 id: 'captions-window-color-%s', 18445 label: 'Color' 18446 }, 18447 18448 // Options for this object are defined below. 18449 windowOpacity: { 18450 selector: '.vjs-window-opacity > select', 18451 id: 'captions-window-opacity-%s', 18452 label: 'Transparency', 18453 options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE] 18454 } 18455 }; 18456 18457 selectConfigs.windowColor.options = selectConfigs.backgroundColor.options; 18458 18459 /** 18460 * Get the actual value of an option. 18461 * 18462 * @param {string} value 18463 * The value to get 18464 * 18465 * @param {Function} [parser] 18466 * Optional function to adjust the value. 18467 * 18468 * @return {Mixed} 18469 * - Will be `undefined` if no value exists 18470 * - Will be `undefined` if the given value is "none". 18471 * - Will be the actual value otherwise. 18472 * 18473 * @private 18474 */ 18475 function parseOptionValue(value, parser) { 18476 if (parser) { 18477 value = parser(value); 18478 } 18479 18480 if (value && value !== 'none') { 18481 return value; 18482 } 18483 } 18484 18485 /** 18486 * Gets the value of the selected <option> element within a <select> element. 18487 * 18488 * @param {Element} el 18489 * the element to look in 18490 * 18491 * @param {Function} [parser] 18492 * Optional function to adjust the value. 18493 * 18494 * @return {Mixed} 18495 * - Will be `undefined` if no value exists 18496 * - Will be `undefined` if the given value is "none". 18497 * - Will be the actual value otherwise. 18498 * 18499 * @private 18500 */ 18501 function getSelectedOptionValue(el, parser) { 18502 var value = el.options[el.options.selectedIndex].value; 18503 18504 return parseOptionValue(value, parser); 18505 } 18506 18507 /** 18508 * Sets the selected <option> element within a <select> element based on a 18509 * given value. 18510 * 18511 * @param {Element} el 18512 * The element to look in. 18513 * 18514 * @param {string} value 18515 * the property to look on. 18516 * 18517 * @param {Function} [parser] 18518 * Optional function to adjust the value before comparing. 18519 * 18520 * @private 18521 */ 18522 function setSelectedOption(el, value, parser) { 18523 if (!value) { 18524 return; 18525 } 18526 18527 for (var i = 0; i < el.options.length; i++) { 18528 if (parseOptionValue(el.options[i].value, parser) === value) { 18529 el.selectedIndex = i; 18530 break; 18531 } 18532 } 18533 } 18534 18535 /** 18536 * Manipulate Text Tracks settings. 18537 * 18538 * @extends ModalDialog 18539 */ 18540 18541 var TextTrackSettings = function (_ModalDialog) { 18542 inherits(TextTrackSettings, _ModalDialog); 18543 18544 /** 18545 * Creates an instance of this class. 18546 * 18547 * @param {Player} player 18548 * The `Player` that this class should be attached to. 18549 * 18550 * @param {Object} [options] 18551 * The key/value store of player options. 18552 */ 18553 function TextTrackSettings(player, options) { 18554 classCallCheck(this, TextTrackSettings); 18555 18556 options.temporary = false; 18557 18558 var _this = possibleConstructorReturn(this, _ModalDialog.call(this, player, options)); 18559 18560 _this.updateDisplay = bind(_this, _this.updateDisplay); 18561 18562 // fill the modal and pretend we have opened it 18563 _this.fill(); 18564 _this.hasBeenOpened_ = _this.hasBeenFilled_ = true; 18565 18566 _this.endDialog = createEl('p', { 18567 className: 'vjs-control-text', 18568 textContent: _this.localize('End of dialog window.') 18569 }); 18570 _this.el().appendChild(_this.endDialog); 18571 18572 _this.setDefaults(); 18573 18574 // Grab `persistTextTrackSettings` from the player options if not passed in child options 18575 if (options.persistTextTrackSettings === undefined) { 18576 _this.options_.persistTextTrackSettings = _this.options_.playerOptions.persistTextTrackSettings; 18577 } 18578 18579 _this.on(_this.$('.vjs-done-button'), 'click', function () { 18580 _this.saveSettings(); 18581 _this.close(); 18582 }); 18583 18584 _this.on(_this.$('.vjs-default-button'), 'click', function () { 18585 _this.setDefaults(); 18586 _this.updateDisplay(); 18587 }); 18588 18589 each(selectConfigs, function (config) { 18590 _this.on(_this.$(config.selector), 'change', _this.updateDisplay); 18591 }); 18592 18593 if (_this.options_.persistTextTrackSettings) { 18594 _this.restoreSettings(); 18595 } 18596 return _this; 18597 } 18598 18599 TextTrackSettings.prototype.dispose = function dispose() { 18600 this.endDialog = null; 18601 18602 _ModalDialog.prototype.dispose.call(this); 18603 }; 18604 18605 /** 18606 * Create a <select> element with configured options. 18607 * 18608 * @param {string} key 18609 * Configuration key to use during creation. 18610 * 18611 * @return {string} 18612 * An HTML string. 18613 * 18614 * @private 18615 */ 18616 18617 18618 TextTrackSettings.prototype.createElSelect_ = function createElSelect_(key) { 18619 var _this2 = this; 18620 18621 var legendId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 18622 var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'label'; 18623 18624 var config = selectConfigs[key]; 18625 var id = config.id.replace('%s', this.id_); 18626 var selectLabelledbyIds = [legendId, id].join(' ').trim(); 18627 18628 return ['<' + type + ' id="' + id + '" class="' + (type === 'label' ? 'vjs-label' : '') + '">', this.localize(config.label), '</' + type + '>', '<select aria-labelledby="' + selectLabelledbyIds + '">'].concat(config.options.map(function (o) { 18629 var optionId = id + '-' + o[1].replace(/\W+/g, ''); 18630 18631 return ['<option id="' + optionId + '" value="' + o[0] + '" ', 'aria-labelledby="' + selectLabelledbyIds + ' ' + optionId + '">', _this2.localize(o[1]), '</option>'].join(''); 18632 })).concat('</select>').join(''); 18633 }; 18634 18635 /** 18636 * Create foreground color element for the component 18637 * 18638 * @return {string} 18639 * An HTML string. 18640 * 18641 * @private 18642 */ 18643 18644 18645 TextTrackSettings.prototype.createElFgColor_ = function createElFgColor_() { 18646 var legendId = 'captions-text-legend-' + this.id_; 18647 18648 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(''); 18649 }; 18650 18651 /** 18652 * Create background color element for the component 18653 * 18654 * @return {string} 18655 * An HTML string. 18656 * 18657 * @private 18658 */ 18659 18660 18661 TextTrackSettings.prototype.createElBgColor_ = function createElBgColor_() { 18662 var legendId = 'captions-background-' + this.id_; 18663 18664 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(''); 18665 }; 18666 18667 /** 18668 * Create window color element for the component 18669 * 18670 * @return {string} 18671 * An HTML string. 18672 * 18673 * @private 18674 */ 18675 18676 18677 TextTrackSettings.prototype.createElWinColor_ = function createElWinColor_() { 18678 var legendId = 'captions-window-' + this.id_; 18679 18680 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(''); 18681 }; 18682 18683 /** 18684 * Create color elements for the component 18685 * 18686 * @return {Element} 18687 * The element that was created 18688 * 18689 * @private 18690 */ 18691 18692 18693 TextTrackSettings.prototype.createElColors_ = function createElColors_() { 18694 return createEl('div', { 18695 className: 'vjs-track-settings-colors', 18696 innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('') 18697 }); 18698 }; 18699 18700 /** 18701 * Create font elements for the component 18702 * 18703 * @return {Element} 18704 * The element that was created. 18705 * 18706 * @private 18707 */ 18708 18709 18710 TextTrackSettings.prototype.createElFont_ = function createElFont_() { 18711 return createEl('div', { 18712 className: 'vjs-track-settings-font', 18713 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('') 18714 }); 18715 }; 18716 18717 /** 18718 * Create controls for the component 18719 * 18720 * @return {Element} 18721 * The element that was created. 18722 * 18723 * @private 18724 */ 18725 18726 18727 TextTrackSettings.prototype.createElControls_ = function createElControls_() { 18728 var defaultsDescription = this.localize('restore all settings to the default values'); 18729 18730 return createEl('div', { 18731 className: 'vjs-track-settings-controls', 18732 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('') 18733 }); 18734 }; 18735 18736 TextTrackSettings.prototype.content = function content() { 18737 return [this.createElColors_(), this.createElFont_(), this.createElControls_()]; 18738 }; 18739 18740 TextTrackSettings.prototype.label = function label() { 18741 return this.localize('Caption Settings Dialog'); 18742 }; 18743 18744 TextTrackSettings.prototype.description = function description() { 18745 return this.localize('Beginning of dialog window. Escape will cancel and close the window.'); 18746 }; 18747 18748 TextTrackSettings.prototype.buildCSSClass = function buildCSSClass() { 18749 return _ModalDialog.prototype.buildCSSClass.call(this) + ' vjs-text-track-settings'; 18750 }; 18751 18752 /** 18753 * Gets an object of text track settings (or null). 18754 * 18755 * @return {Object} 18756 * An object with config values parsed from the DOM or localStorage. 18757 */ 18758 18759 18760 TextTrackSettings.prototype.getValues = function getValues() { 18761 var _this3 = this; 18762 18763 return reduce(selectConfigs, function (accum, config, key) { 18764 var value = getSelectedOptionValue(_this3.$(config.selector), config.parser); 18765 18766 if (value !== undefined) { 18767 accum[key] = value; 18768 } 18769 18770 return accum; 18771 }, {}); 18772 }; 18773 18774 /** 18775 * Sets text track settings from an object of values. 18776 * 18777 * @param {Object} values 18778 * An object with config values parsed from the DOM or localStorage. 18779 */ 18780 18781 18782 TextTrackSettings.prototype.setValues = function setValues(values) { 18783 var _this4 = this; 18784 18785 each(selectConfigs, function (config, key) { 18786 setSelectedOption(_this4.$(config.selector), values[key], config.parser); 18787 }); 18788 }; 18789 18790 /** 18791 * Sets all `<select>` elements to their default values. 18792 */ 18793 18794 18795 TextTrackSettings.prototype.setDefaults = function setDefaults() { 18796 var _this5 = this; 18797 18798 each(selectConfigs, function (config) { 18799 var index = config.hasOwnProperty('default') ? config.default : 0; 18800 18801 _this5.$(config.selector).selectedIndex = index; 18802 }); 18803 }; 18804 18805 /** 18806 * Restore texttrack settings from localStorage 18807 */ 18808 18809 18810 TextTrackSettings.prototype.restoreSettings = function restoreSettings() { 18811 var values = void 0; 18812 18813 try { 18814 values = JSON.parse(window_1.localStorage.getItem(LOCAL_STORAGE_KEY)); 18815 } catch (err) { 18816 log$1.warn(err); 18817 } 18818 18819 if (values) { 18820 this.setValues(values); 18821 } 18822 }; 18823 18824 /** 18825 * Save text track settings to localStorage 18826 */ 18827 18828 18829 TextTrackSettings.prototype.saveSettings = function saveSettings() { 18830 if (!this.options_.persistTextTrackSettings) { 18831 return; 18832 } 18833 18834 var values = this.getValues(); 18835 18836 try { 18837 if (Object.keys(values).length) { 18838 window_1.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values)); 18839 } else { 18840 window_1.localStorage.removeItem(LOCAL_STORAGE_KEY); 18841 } 18842 } catch (err) { 18843 log$1.warn(err); 18844 } 18845 }; 18846 18847 /** 18848 * Update display of text track settings 18849 */ 18850 18851 18852 TextTrackSettings.prototype.updateDisplay = function updateDisplay() { 18853 var ttDisplay = this.player_.getChild('textTrackDisplay'); 18854 18855 if (ttDisplay) { 18856 ttDisplay.updateDisplay(); 18857 } 18858 }; 18859 18860 /** 18861 * conditionally blur the element and refocus the captions button 18862 * 18863 * @private 18864 */ 18865 18866 18867 TextTrackSettings.prototype.conditionalBlur_ = function conditionalBlur_() { 18868 this.previouslyActiveEl_ = null; 18869 this.off(document_1, 'keydown', this.handleKeyDown); 18870 18871 var cb = this.player_.controlBar; 18872 var subsCapsBtn = cb && cb.subsCapsButton; 18873 var ccBtn = cb && cb.captionsButton; 18874 18875 if (subsCapsBtn) { 18876 subsCapsBtn.focus(); 18877 } else if (ccBtn) { 18878 ccBtn.focus(); 18879 } 18880 }; 18881 18882 return TextTrackSettings; 18883 }(ModalDialog); 18884 18885 Component.registerComponent('TextTrackSettings', TextTrackSettings); 18886 18887 /** 18888 * @file resize-manager.js 18889 */ 18890 18891 /** 18892 * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions. 18893 * 18894 * 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}. 18895 * 18896 * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option. 18897 * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below. 18898 * @example <caption>How to disable the resize manager</caption> 18899 * const player = videojs('#vid', { 18900 * resizeManager: false 18901 * }); 18902 * 18903 * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification} 18904 * 18905 * @extends Component 18906 */ 18907 18908 var ResizeManager = function (_Component) { 18909 inherits(ResizeManager, _Component); 18910 18911 /** 18912 * Create the ResizeManager. 18913 * 18914 * @param {Object} player 18915 * The `Player` that this class should be attached to. 18916 * 18917 * @param {Object} [options] 18918 * The key/value store of ResizeManager options. 18919 * 18920 * @param {Object} [options.ResizeObserver] 18921 * A polyfill for ResizeObserver can be passed in here. 18922 * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback. 18923 */ 18924 function ResizeManager(player, options) { 18925 classCallCheck(this, ResizeManager); 18926 18927 var RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window_1.ResizeObserver; 18928 18929 // if `null` was passed, we want to disable the ResizeObserver 18930 if (options.ResizeObserver === null) { 18931 RESIZE_OBSERVER_AVAILABLE = false; 18932 } 18933 18934 // Only create an element when ResizeObserver isn't available 18935 var options_ = mergeOptions({ createEl: !RESIZE_OBSERVER_AVAILABLE }, options); 18936 18937 var _this = possibleConstructorReturn(this, _Component.call(this, player, options_)); 18938 18939 _this.ResizeObserver = options.ResizeObserver || window_1.ResizeObserver; 18940 _this.loadListener_ = null; 18941 _this.resizeObserver_ = null; 18942 _this.debouncedHandler_ = debounce(function () { 18943 _this.resizeHandler(); 18944 }, 100, false, player); 18945 18946 if (RESIZE_OBSERVER_AVAILABLE) { 18947 _this.resizeObserver_ = new _this.ResizeObserver(_this.debouncedHandler_); 18948 _this.resizeObserver_.observe(player.el()); 18949 } else { 18950 _this.loadListener_ = function () { 18951 if (_this.el_.contentWindow) { 18952 on(_this.el_.contentWindow, 'resize', _this.debouncedHandler_); 18953 } 18954 _this.off('load', _this.loadListener_); 18955 }; 18956 18957 _this.on('load', _this.loadListener_); 18958 } 18959 return _this; 18960 } 18961 18962 ResizeManager.prototype.createEl = function createEl() { 18963 return _Component.prototype.createEl.call(this, 'iframe', { 18964 className: 'vjs-resize-manager' 18965 }); 18966 }; 18967 18968 /** 18969 * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver 18970 * 18971 * @fires Player#playerresize 18972 */ 18973 18974 18975 ResizeManager.prototype.resizeHandler = function resizeHandler() { 18976 /** 18977 * Called when the player size has changed 18978 * 18979 * @event Player#playerresize 18980 * @type {EventTarget~Event} 18981 */ 18982 this.player_.trigger('playerresize'); 18983 }; 18984 18985 ResizeManager.prototype.dispose = function dispose() { 18986 if (this.resizeObserver_) { 18987 if (this.player_.el()) { 18988 this.resizeObserver_.unobserve(this.player_.el()); 18989 } 18990 this.resizeObserver_.disconnect(); 18991 } 18992 18993 if (this.el_ && this.el_.contentWindow) { 18994 off(this.el_.contentWindow, 'resize', this.debouncedHandler_); 18995 } 18996 18997 if (this.loadListener_) { 18998 this.off('load', this.loadListener_); 18999 } 19000 19001 this.ResizeObserver = null; 19002 this.resizeObserver = null; 19003 this.debouncedHandler_ = null; 19004 this.loadListener_ = null; 19005 }; 19006 19007 return ResizeManager; 19008 }(Component); 19009 19010 Component.registerComponent('ResizeManager', ResizeManager); 19011 19012 /** 19013 * This function is used to fire a sourceset when there is something 19014 * similar to `mediaEl.load()` being called. It will try to find the source via 19015 * the `src` attribute and then the `<source>` elements. It will then fire `sourceset` 19016 * with the source that was found or empty string if we cannot know. If it cannot 19017 * find a source then `sourceset` will not be fired. 19018 * 19019 * @param {Html5} tech 19020 * The tech object that sourceset was setup on 19021 * 19022 * @return {boolean} 19023 * returns false if the sourceset was not fired and true otherwise. 19024 */ 19025 var sourcesetLoad = function sourcesetLoad(tech) { 19026 var el = tech.el(); 19027 19028 // if `el.src` is set, that source will be loaded. 19029 if (el.hasAttribute('src')) { 19030 tech.triggerSourceset(el.src); 19031 return true; 19032 } 19033 19034 /** 19035 * Since there isn't a src property on the media element, source elements will be used for 19036 * implementing the source selection algorithm. This happens asynchronously and 19037 * for most cases were there is more than one source we cannot tell what source will 19038 * be loaded, without re-implementing the source selection algorithm. At this time we are not 19039 * going to do that. There are three special cases that we do handle here though: 19040 * 19041 * 1. If there are no sources, do not fire `sourceset`. 19042 * 2. If there is only one `<source>` with a `src` property/attribute that is our `src` 19043 * 3. If there is more than one `<source>` but all of them have the same `src` url. 19044 * That will be our src. 19045 */ 19046 var sources = tech.$$('source'); 19047 var srcUrls = []; 19048 var src = ''; 19049 19050 // if there are no sources, do not fire sourceset 19051 if (!sources.length) { 19052 return false; 19053 } 19054 19055 // only count valid/non-duplicate source elements 19056 for (var i = 0; i < sources.length; i++) { 19057 var url = sources[i].src; 19058 19059 if (url && srcUrls.indexOf(url) === -1) { 19060 srcUrls.push(url); 19061 } 19062 } 19063 19064 // there were no valid sources 19065 if (!srcUrls.length) { 19066 return false; 19067 } 19068 19069 // there is only one valid source element url 19070 // use that 19071 if (srcUrls.length === 1) { 19072 src = srcUrls[0]; 19073 } 19074 19075 tech.triggerSourceset(src); 19076 return true; 19077 }; 19078 19079 /** 19080 * our implementation of an `innerHTML` descriptor for browsers 19081 * that do not have one. 19082 */ 19083 var innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', { 19084 get: function get() { 19085 return this.cloneNode(true).innerHTML; 19086 }, 19087 set: function set(v) { 19088 // make a dummy node to use innerHTML on 19089 var dummy = document_1.createElement(this.nodeName.toLowerCase()); 19090 19091 // set innerHTML to the value provided 19092 dummy.innerHTML = v; 19093 19094 // make a document fragment to hold the nodes from dummy 19095 var docFrag = document_1.createDocumentFragment(); 19096 19097 // copy all of the nodes created by the innerHTML on dummy 19098 // to the document fragment 19099 while (dummy.childNodes.length) { 19100 docFrag.appendChild(dummy.childNodes[0]); 19101 } 19102 19103 // remove content 19104 this.innerText = ''; 19105 19106 // now we add all of that html in one by appending the 19107 // document fragment. This is how innerHTML does it. 19108 window_1.Element.prototype.appendChild.call(this, docFrag); 19109 19110 // then return the result that innerHTML's setter would 19111 return this.innerHTML; 19112 } 19113 }); 19114 19115 /** 19116 * Get a property descriptor given a list of priorities and the 19117 * property to get. 19118 */ 19119 var getDescriptor = function getDescriptor(priority, prop) { 19120 var descriptor = {}; 19121 19122 for (var i = 0; i < priority.length; i++) { 19123 descriptor = Object.getOwnPropertyDescriptor(priority[i], prop); 19124 19125 if (descriptor && descriptor.set && descriptor.get) { 19126 break; 19127 } 19128 } 19129 19130 descriptor.enumerable = true; 19131 descriptor.configurable = true; 19132 19133 return descriptor; 19134 }; 19135 19136 var getInnerHTMLDescriptor = function getInnerHTMLDescriptor(tech) { 19137 return getDescriptor([tech.el(), window_1.HTMLMediaElement.prototype, window_1.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML'); 19138 }; 19139 19140 /** 19141 * Patches browser internal functions so that we can tell synchronously 19142 * if a `<source>` was appended to the media element. For some reason this 19143 * causes a `sourceset` if the the media element is ready and has no source. 19144 * This happens when: 19145 * - The page has just loaded and the media element does not have a source. 19146 * - The media element was emptied of all sources, then `load()` was called. 19147 * 19148 * It does this by patching the following functions/properties when they are supported: 19149 * 19150 * - `append()` - can be used to add a `<source>` element to the media element 19151 * - `appendChild()` - can be used to add a `<source>` element to the media element 19152 * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element 19153 * - `innerHTML` - can be used to add a `<source>` element to the media element 19154 * 19155 * @param {Html5} tech 19156 * The tech object that sourceset is being setup on. 19157 */ 19158 var firstSourceWatch = function firstSourceWatch(tech) { 19159 var el = tech.el(); 19160 19161 // make sure firstSourceWatch isn't setup twice. 19162 if (el.resetSourceWatch_) { 19163 return; 19164 } 19165 19166 var old = {}; 19167 var innerDescriptor = getInnerHTMLDescriptor(tech); 19168 var appendWrapper = function appendWrapper(appendFn) { 19169 return function () { 19170 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 19171 args[_key] = arguments[_key]; 19172 } 19173 19174 var retval = appendFn.apply(el, args); 19175 19176 sourcesetLoad(tech); 19177 19178 return retval; 19179 }; 19180 }; 19181 19182 ['append', 'appendChild', 'insertAdjacentHTML'].forEach(function (k) { 19183 if (!el[k]) { 19184 return; 19185 } 19186 19187 // store the old function 19188 old[k] = el[k]; 19189 19190 // call the old function with a sourceset if a source 19191 // was loaded 19192 el[k] = appendWrapper(old[k]); 19193 }); 19194 19195 Object.defineProperty(el, 'innerHTML', mergeOptions(innerDescriptor, { 19196 set: appendWrapper(innerDescriptor.set) 19197 })); 19198 19199 el.resetSourceWatch_ = function () { 19200 el.resetSourceWatch_ = null; 19201 Object.keys(old).forEach(function (k) { 19202 el[k] = old[k]; 19203 }); 19204 19205 Object.defineProperty(el, 'innerHTML', innerDescriptor); 19206 }; 19207 19208 // on the first sourceset, we need to revert our changes 19209 tech.one('sourceset', el.resetSourceWatch_); 19210 }; 19211 19212 /** 19213 * our implementation of a `src` descriptor for browsers 19214 * that do not have one. 19215 */ 19216 var srcDescriptorPolyfill = Object.defineProperty({}, 'src', { 19217 get: function get() { 19218 if (this.hasAttribute('src')) { 19219 return getAbsoluteURL(window_1.Element.prototype.getAttribute.call(this, 'src')); 19220 } 19221 19222 return ''; 19223 }, 19224 set: function set(v) { 19225 window_1.Element.prototype.setAttribute.call(this, 'src', v); 19226 19227 return v; 19228 } 19229 }); 19230 19231 var getSrcDescriptor = function getSrcDescriptor(tech) { 19232 return getDescriptor([tech.el(), window_1.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src'); 19233 }; 19234 19235 /** 19236 * setup `sourceset` handling on the `Html5` tech. This function 19237 * patches the following element properties/functions: 19238 * 19239 * - `src` - to determine when `src` is set 19240 * - `setAttribute()` - to determine when `src` is set 19241 * - `load()` - this re-triggers the source selection algorithm, and can 19242 * cause a sourceset. 19243 * 19244 * If there is no source when we are adding `sourceset` support or during a `load()` 19245 * we also patch the functions listed in `firstSourceWatch`. 19246 * 19247 * @param {Html5} tech 19248 * The tech to patch 19249 */ 19250 var setupSourceset = function setupSourceset(tech) { 19251 if (!tech.featuresSourceset) { 19252 return; 19253 } 19254 19255 var el = tech.el(); 19256 19257 // make sure sourceset isn't setup twice. 19258 if (el.resetSourceset_) { 19259 return; 19260 } 19261 19262 var srcDescriptor = getSrcDescriptor(tech); 19263 var oldSetAttribute = el.setAttribute; 19264 var oldLoad = el.load; 19265 19266 Object.defineProperty(el, 'src', mergeOptions(srcDescriptor, { 19267 set: function set(v) { 19268 var retval = srcDescriptor.set.call(el, v); 19269 19270 // we use the getter here to get the actual value set on src 19271 tech.triggerSourceset(el.src); 19272 19273 return retval; 19274 } 19275 })); 19276 19277 el.setAttribute = function (n, v) { 19278 var retval = oldSetAttribute.call(el, n, v); 19279 19280 if (/src/i.test(n)) { 19281 tech.triggerSourceset(el.src); 19282 } 19283 19284 return retval; 19285 }; 19286 19287 el.load = function () { 19288 var retval = oldLoad.call(el); 19289 19290 // if load was called, but there was no source to fire 19291 // sourceset on. We have to watch for a source append 19292 // as that can trigger a `sourceset` when the media element 19293 // has no source 19294 if (!sourcesetLoad(tech)) { 19295 tech.triggerSourceset(''); 19296 firstSourceWatch(tech); 19297 } 19298 19299 return retval; 19300 }; 19301 19302 if (el.currentSrc) { 19303 tech.triggerSourceset(el.currentSrc); 19304 } else if (!sourcesetLoad(tech)) { 19305 firstSourceWatch(tech); 19306 } 19307 19308 el.resetSourceset_ = function () { 19309 el.resetSourceset_ = null; 19310 el.load = oldLoad; 19311 el.setAttribute = oldSetAttribute; 19312 Object.defineProperty(el, 'src', srcDescriptor); 19313 if (el.resetSourceWatch_) { 19314 el.resetSourceWatch_(); 19315 } 19316 }; 19317 }; 19318 19319 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.']); 19320 19321 /** 19322 * HTML5 Media Controller - Wrapper for HTML5 Media API 19323 * 19324 * @mixes Tech~SourceHandlerAdditions 19325 * @extends Tech 19326 */ 19327 19328 var Html5 = function (_Tech) { 19329 inherits(Html5, _Tech); 19330 19331 /** 19332 * Create an instance of this Tech. 19333 * 19334 * @param {Object} [options] 19335 * The key/value store of player options. 19336 * 19337 * @param {Component~ReadyCallback} ready 19338 * Callback function to call when the `HTML5` Tech is ready. 19339 */ 19340 function Html5(options, ready) { 19341 classCallCheck(this, Html5); 19342 19343 var _this = possibleConstructorReturn(this, _Tech.call(this, options, ready)); 19344 19345 var source = options.source; 19346 var crossoriginTracks = false; 19347 19348 // Set the source if one is provided 19349 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) 19350 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source 19351 // anyway so the error gets fired. 19352 if (source && (_this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) { 19353 _this.setSource(source); 19354 } else { 19355 _this.handleLateInit_(_this.el_); 19356 } 19357 19358 // setup sourceset after late sourceset/init 19359 if (options.enableSourceset) { 19360 _this.setupSourcesetHandling_(); 19361 } 19362 19363 if (_this.el_.hasChildNodes()) { 19364 19365 var nodes = _this.el_.childNodes; 19366 var nodesLength = nodes.length; 19367 var removeNodes = []; 19368 19369 while (nodesLength--) { 19370 var node = nodes[nodesLength]; 19371 var nodeName = node.nodeName.toLowerCase(); 19372 19373 if (nodeName === 'track') { 19374 if (!_this.featuresNativeTextTracks) { 19375 // Empty video tag tracks so the built-in player doesn't use them also. 19376 // This may not be fast enough to stop HTML5 browsers from reading the tags 19377 // so we'll need to turn off any default tracks if we're manually doing 19378 // captions and subtitles. videoElement.textTracks 19379 removeNodes.push(node); 19380 } else { 19381 // store HTMLTrackElement and TextTrack to remote list 19382 _this.remoteTextTrackEls().addTrackElement_(node); 19383 _this.remoteTextTracks().addTrack(node.track); 19384 _this.textTracks().addTrack(node.track); 19385 if (!crossoriginTracks && !_this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) { 19386 crossoriginTracks = true; 19387 } 19388 } 19389 } 19390 } 19391 19392 for (var i = 0; i < removeNodes.length; i++) { 19393 _this.el_.removeChild(removeNodes[i]); 19394 } 19395 } 19396 19397 _this.proxyNativeTracks_(); 19398 if (_this.featuresNativeTextTracks && crossoriginTracks) { 19399 log$1.warn(tsml(_templateObject$1)); 19400 } 19401 19402 // prevent iOS Safari from disabling metadata text tracks during native playback 19403 _this.restoreMetadataTracksInIOSNativePlayer_(); 19404 19405 // Determine if native controls should be used 19406 // Our goal should be to get the custom controls on mobile solid everywhere 19407 // so we can remove this all together. Right now this will block custom 19408 // controls on touch enabled laptops like the Chrome Pixel 19409 if ((TOUCH_ENABLED || IS_IPHONE || IS_NATIVE_ANDROID) && options.nativeControlsForTouch === true) { 19410 _this.setControls(true); 19411 } 19412 19413 // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` 19414 // into a `fullscreenchange` event 19415 _this.proxyWebkitFullscreen_(); 19416 19417 _this.triggerReady(); 19418 return _this; 19419 } 19420 19421 /** 19422 * Dispose of `HTML5` media element and remove all tracks. 19423 */ 19424 19425 19426 Html5.prototype.dispose = function dispose() { 19427 if (this.el_ && this.el_.resetSourceset_) { 19428 this.el_.resetSourceset_(); 19429 } 19430 Html5.disposeMediaElement(this.el_); 19431 this.options_ = null; 19432 19433 // tech will handle clearing of the emulated track list 19434 _Tech.prototype.dispose.call(this); 19435 }; 19436 19437 /** 19438 * Modify the media element so that we can detect when 19439 * the source is changed. Fires `sourceset` just after the source has changed 19440 */ 19441 19442 19443 Html5.prototype.setupSourcesetHandling_ = function setupSourcesetHandling_() { 19444 setupSourceset(this); 19445 }; 19446 19447 /** 19448 * When a captions track is enabled in the iOS Safari native player, all other 19449 * tracks are disabled (including metadata tracks), which nulls all of their 19450 * associated cue points. This will restore metadata tracks to their pre-fullscreen 19451 * state in those cases so that cue points are not needlessly lost. 19452 * 19453 * @private 19454 */ 19455 19456 19457 Html5.prototype.restoreMetadataTracksInIOSNativePlayer_ = function restoreMetadataTracksInIOSNativePlayer_() { 19458 var textTracks = this.textTracks(); 19459 var metadataTracksPreFullscreenState = void 0; 19460 19461 // captures a snapshot of every metadata track's current state 19462 var takeMetadataTrackSnapshot = function takeMetadataTrackSnapshot() { 19463 metadataTracksPreFullscreenState = []; 19464 19465 for (var i = 0; i < textTracks.length; i++) { 19466 var track = textTracks[i]; 19467 19468 if (track.kind === 'metadata') { 19469 metadataTracksPreFullscreenState.push({ 19470 track: track, 19471 storedMode: track.mode 19472 }); 19473 } 19474 } 19475 }; 19476 19477 // snapshot each metadata track's initial state, and update the snapshot 19478 // each time there is a track 'change' event 19479 takeMetadataTrackSnapshot(); 19480 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 19481 19482 this.on('dispose', function () { 19483 return textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 19484 }); 19485 19486 var restoreTrackMode = function restoreTrackMode() { 19487 for (var i = 0; i < metadataTracksPreFullscreenState.length; i++) { 19488 var storedTrack = metadataTracksPreFullscreenState[i]; 19489 19490 if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) { 19491 storedTrack.track.mode = storedTrack.storedMode; 19492 } 19493 } 19494 // we only want this handler to be executed on the first 'change' event 19495 textTracks.removeEventListener('change', restoreTrackMode); 19496 }; 19497 19498 // when we enter fullscreen playback, stop updating the snapshot and 19499 // restore all track modes to their pre-fullscreen state 19500 this.on('webkitbeginfullscreen', function () { 19501 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 19502 19503 // remove the listener before adding it just in case it wasn't previously removed 19504 textTracks.removeEventListener('change', restoreTrackMode); 19505 textTracks.addEventListener('change', restoreTrackMode); 19506 }); 19507 19508 // start updating the snapshot again after leaving fullscreen 19509 this.on('webkitendfullscreen', function () { 19510 // remove the listener before adding it just in case it wasn't previously removed 19511 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 19512 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 19513 19514 // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback 19515 textTracks.removeEventListener('change', restoreTrackMode); 19516 }); 19517 }; 19518 19519 /** 19520 * Attempt to force override of tracks for the given type 19521 * 19522 * @param {String} type - Track type to override, possible values include 'Audio', 19523 * 'Video', and 'Text'. 19524 * @param {Boolean} override - If set to true native audio/video will be overridden, 19525 * otherwise native audio/video will potentially be used. 19526 * @private 19527 */ 19528 19529 19530 Html5.prototype.overrideNative_ = function overrideNative_(type, override) { 19531 var _this2 = this; 19532 19533 // If there is no behavioral change don't add/remove listeners 19534 if (override !== this['featuresNative' + type + 'Tracks']) { 19535 return; 19536 } 19537 19538 var lowerCaseType = type.toLowerCase(); 19539 19540 if (this[lowerCaseType + 'TracksListeners_']) { 19541 Object.keys(this[lowerCaseType + 'TracksListeners_']).forEach(function (eventName) { 19542 var elTracks = _this2.el()[lowerCaseType + 'Tracks']; 19543 19544 elTracks.removeEventListener(eventName, _this2[lowerCaseType + 'TracksListeners_'][eventName]); 19545 }); 19546 } 19547 19548 this['featuresNative' + type + 'Tracks'] = !override; 19549 this[lowerCaseType + 'TracksListeners_'] = null; 19550 19551 this.proxyNativeTracksForType_(lowerCaseType); 19552 }; 19553 19554 /** 19555 * Attempt to force override of native audio tracks. 19556 * 19557 * @param {Boolean} override - If set to true native audio will be overridden, 19558 * otherwise native audio will potentially be used. 19559 */ 19560 19561 19562 Html5.prototype.overrideNativeAudioTracks = function overrideNativeAudioTracks(override) { 19563 this.overrideNative_('Audio', override); 19564 }; 19565 19566 /** 19567 * Attempt to force override of native video tracks. 19568 * 19569 * @param {Boolean} override - If set to true native video will be overridden, 19570 * otherwise native video will potentially be used. 19571 */ 19572 19573 19574 Html5.prototype.overrideNativeVideoTracks = function overrideNativeVideoTracks(override) { 19575 this.overrideNative_('Video', override); 19576 }; 19577 19578 /** 19579 * Proxy native track list events for the given type to our track 19580 * lists if the browser we are playing in supports that type of track list. 19581 * 19582 * @param {string} name - Track type; values include 'audio', 'video', and 'text' 19583 * @private 19584 */ 19585 19586 19587 Html5.prototype.proxyNativeTracksForType_ = function proxyNativeTracksForType_(name) { 19588 var _this3 = this; 19589 19590 var props = NORMAL[name]; 19591 var elTracks = this.el()[props.getterName]; 19592 var techTracks = this[props.getterName](); 19593 19594 if (!this['featuresNative' + props.capitalName + 'Tracks'] || !elTracks || !elTracks.addEventListener) { 19595 return; 19596 } 19597 var listeners = { 19598 change: function change(e) { 19599 techTracks.trigger({ 19600 type: 'change', 19601 target: techTracks, 19602 currentTarget: techTracks, 19603 srcElement: techTracks 19604 }); 19605 }, 19606 addtrack: function addtrack(e) { 19607 techTracks.addTrack(e.track); 19608 }, 19609 removetrack: function removetrack(e) { 19610 techTracks.removeTrack(e.track); 19611 } 19612 }; 19613 var removeOldTracks = function removeOldTracks() { 19614 var removeTracks = []; 19615 19616 for (var i = 0; i < techTracks.length; i++) { 19617 var found = false; 19618 19619 for (var j = 0; j < elTracks.length; j++) { 19620 if (elTracks[j] === techTracks[i]) { 19621 found = true; 19622 break; 19623 } 19624 } 19625 19626 if (!found) { 19627 removeTracks.push(techTracks[i]); 19628 } 19629 } 19630 19631 while (removeTracks.length) { 19632 techTracks.removeTrack(removeTracks.shift()); 19633 } 19634 }; 19635 19636 this[props.getterName + 'Listeners_'] = listeners; 19637 19638 Object.keys(listeners).forEach(function (eventName) { 19639 var listener = listeners[eventName]; 19640 19641 elTracks.addEventListener(eventName, listener); 19642 _this3.on('dispose', function (e) { 19643 return elTracks.removeEventListener(eventName, listener); 19644 }); 19645 }); 19646 19647 // Remove (native) tracks that are not used anymore 19648 this.on('loadstart', removeOldTracks); 19649 this.on('dispose', function (e) { 19650 return _this3.off('loadstart', removeOldTracks); 19651 }); 19652 }; 19653 19654 /** 19655 * Proxy all native track list events to our track lists if the browser we are playing 19656 * in supports that type of track list. 19657 * 19658 * @private 19659 */ 19660 19661 19662 Html5.prototype.proxyNativeTracks_ = function proxyNativeTracks_() { 19663 var _this4 = this; 19664 19665 NORMAL.names.forEach(function (name) { 19666 _this4.proxyNativeTracksForType_(name); 19667 }); 19668 }; 19669 19670 /** 19671 * Create the `Html5` Tech's DOM element. 19672 * 19673 * @return {Element} 19674 * The element that gets created. 19675 */ 19676 19677 19678 Html5.prototype.createEl = function createEl$$1() { 19679 var el = this.options_.tag; 19680 19681 // Check if this browser supports moving the element into the box. 19682 // On the iPhone video will break if you move the element, 19683 // So we have to create a brand new element. 19684 // If we ingested the player div, we do not need to move the media element. 19685 if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) { 19686 19687 // If the original tag is still there, clone and remove it. 19688 if (el) { 19689 var clone = el.cloneNode(true); 19690 19691 if (el.parentNode) { 19692 el.parentNode.insertBefore(clone, el); 19693 } 19694 Html5.disposeMediaElement(el); 19695 el = clone; 19696 } else { 19697 el = document_1.createElement('video'); 19698 19699 // determine if native controls should be used 19700 var tagAttributes = this.options_.tag && getAttributes(this.options_.tag); 19701 var attributes = mergeOptions({}, tagAttributes); 19702 19703 if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { 19704 delete attributes.controls; 19705 } 19706 19707 setAttributes(el, assign(attributes, { 19708 id: this.options_.techId, 19709 class: 'vjs-tech' 19710 })); 19711 } 19712 19713 el.playerId = this.options_.playerId; 19714 } 19715 19716 if (typeof this.options_.preload !== 'undefined') { 19717 setAttribute(el, 'preload', this.options_.preload); 19718 } 19719 19720 // Update specific tag settings, in case they were overridden 19721 // `autoplay` has to be *last* so that `muted` and `playsinline` are present 19722 // when iOS/Safari or other browsers attempt to autoplay. 19723 var settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay']; 19724 19725 for (var i = 0; i < settingsAttrs.length; i++) { 19726 var attr = settingsAttrs[i]; 19727 var value = this.options_[attr]; 19728 19729 if (typeof value !== 'undefined') { 19730 if (value) { 19731 setAttribute(el, attr, attr); 19732 } else { 19733 removeAttribute(el, attr); 19734 } 19735 el[attr] = value; 19736 } 19737 } 19738 19739 return el; 19740 }; 19741 19742 /** 19743 * This will be triggered if the loadstart event has already fired, before videojs was 19744 * ready. Two known examples of when this can happen are: 19745 * 1. If we're loading the playback object after it has started loading 19746 * 2. The media is already playing the (often with autoplay on) then 19747 * 19748 * This function will fire another loadstart so that videojs can catchup. 19749 * 19750 * @fires Tech#loadstart 19751 * 19752 * @return {undefined} 19753 * returns nothing. 19754 */ 19755 19756 19757 Html5.prototype.handleLateInit_ = function handleLateInit_(el) { 19758 if (el.networkState === 0 || el.networkState === 3) { 19759 // The video element hasn't started loading the source yet 19760 // or didn't find a source 19761 return; 19762 } 19763 19764 if (el.readyState === 0) { 19765 // NetworkState is set synchronously BUT loadstart is fired at the 19766 // end of the current stack, usually before setInterval(fn, 0). 19767 // So at this point we know loadstart may have already fired or is 19768 // about to fire, and either way the player hasn't seen it yet. 19769 // We don't want to fire loadstart prematurely here and cause a 19770 // double loadstart so we'll wait and see if it happens between now 19771 // and the next loop, and fire it if not. 19772 // HOWEVER, we also want to make sure it fires before loadedmetadata 19773 // which could also happen between now and the next loop, so we'll 19774 // watch for that also. 19775 var loadstartFired = false; 19776 var setLoadstartFired = function setLoadstartFired() { 19777 loadstartFired = true; 19778 }; 19779 19780 this.on('loadstart', setLoadstartFired); 19781 19782 var triggerLoadstart = function triggerLoadstart() { 19783 // We did miss the original loadstart. Make sure the player 19784 // sees loadstart before loadedmetadata 19785 if (!loadstartFired) { 19786 this.trigger('loadstart'); 19787 } 19788 }; 19789 19790 this.on('loadedmetadata', triggerLoadstart); 19791 19792 this.ready(function () { 19793 this.off('loadstart', setLoadstartFired); 19794 this.off('loadedmetadata', triggerLoadstart); 19795 19796 if (!loadstartFired) { 19797 // We did miss the original native loadstart. Fire it now. 19798 this.trigger('loadstart'); 19799 } 19800 }); 19801 19802 return; 19803 } 19804 19805 // From here on we know that loadstart already fired and we missed it. 19806 // The other readyState events aren't as much of a problem if we double 19807 // them, so not going to go to as much trouble as loadstart to prevent 19808 // that unless we find reason to. 19809 var eventsToTrigger = ['loadstart']; 19810 19811 // loadedmetadata: newly equal to HAVE_METADATA (1) or greater 19812 eventsToTrigger.push('loadedmetadata'); 19813 19814 // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater 19815 if (el.readyState >= 2) { 19816 eventsToTrigger.push('loadeddata'); 19817 } 19818 19819 // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater 19820 if (el.readyState >= 3) { 19821 eventsToTrigger.push('canplay'); 19822 } 19823 19824 // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4) 19825 if (el.readyState >= 4) { 19826 eventsToTrigger.push('canplaythrough'); 19827 } 19828 19829 // We still need to give the player time to add event listeners 19830 this.ready(function () { 19831 eventsToTrigger.forEach(function (type) { 19832 this.trigger(type); 19833 }, this); 19834 }); 19835 }; 19836 19837 /** 19838 * Set current time for the `HTML5` tech. 19839 * 19840 * @param {number} seconds 19841 * Set the current time of the media to this. 19842 */ 19843 19844 19845 Html5.prototype.setCurrentTime = function setCurrentTime(seconds) { 19846 try { 19847 this.el_.currentTime = seconds; 19848 } catch (e) { 19849 log$1(e, 'Video is not ready. (Video.js)'); 19850 // this.warning(VideoJS.warnings.videoNotReady); 19851 } 19852 }; 19853 19854 /** 19855 * Get the current duration of the HTML5 media element. 19856 * 19857 * @return {number} 19858 * The duration of the media or 0 if there is no duration. 19859 */ 19860 19861 19862 Html5.prototype.duration = function duration() { 19863 var _this5 = this; 19864 19865 // Android Chrome will report duration as Infinity for VOD HLS until after 19866 // playback has started, which triggers the live display erroneously. 19867 // Return NaN if playback has not started and trigger a durationupdate once 19868 // the duration can be reliably known. 19869 if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) { 19870 // Wait for the first `timeupdate` with currentTime > 0 - there may be 19871 // several with 0 19872 var checkProgress = function checkProgress() { 19873 if (_this5.el_.currentTime > 0) { 19874 // Trigger durationchange for genuinely live video 19875 if (_this5.el_.duration === Infinity) { 19876 _this5.trigger('durationchange'); 19877 } 19878 _this5.off('timeupdate', checkProgress); 19879 } 19880 }; 19881 19882 this.on('timeupdate', checkProgress); 19883 return NaN; 19884 } 19885 return this.el_.duration || NaN; 19886 }; 19887 19888 /** 19889 * Get the current width of the HTML5 media element. 19890 * 19891 * @return {number} 19892 * The width of the HTML5 media element. 19893 */ 19894 19895 19896 Html5.prototype.width = function width() { 19897 return this.el_.offsetWidth; 19898 }; 19899 19900 /** 19901 * Get the current height of the HTML5 media element. 19902 * 19903 * @return {number} 19904 * The height of the HTML5 media element. 19905 */ 19906 19907 19908 Html5.prototype.height = function height() { 19909 return this.el_.offsetHeight; 19910 }; 19911 19912 /** 19913 * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into 19914 * `fullscreenchange` event. 19915 * 19916 * @private 19917 * @fires fullscreenchange 19918 * @listens webkitendfullscreen 19919 * @listens webkitbeginfullscreen 19920 * @listens webkitbeginfullscreen 19921 */ 19922 19923 19924 Html5.prototype.proxyWebkitFullscreen_ = function proxyWebkitFullscreen_() { 19925 var _this6 = this; 19926 19927 if (!('webkitDisplayingFullscreen' in this.el_)) { 19928 return; 19929 } 19930 19931 var endFn = function endFn() { 19932 this.trigger('fullscreenchange', { isFullscreen: false }); 19933 }; 19934 19935 var beginFn = function beginFn() { 19936 if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') { 19937 this.one('webkitendfullscreen', endFn); 19938 19939 this.trigger('fullscreenchange', { isFullscreen: true }); 19940 } 19941 }; 19942 19943 this.on('webkitbeginfullscreen', beginFn); 19944 this.on('dispose', function () { 19945 _this6.off('webkitbeginfullscreen', beginFn); 19946 _this6.off('webkitendfullscreen', endFn); 19947 }); 19948 }; 19949 19950 /** 19951 * Check if fullscreen is supported on the current playback device. 19952 * 19953 * @return {boolean} 19954 * - True if fullscreen is supported. 19955 * - False if fullscreen is not supported. 19956 */ 19957 19958 19959 Html5.prototype.supportsFullScreen = function supportsFullScreen() { 19960 if (typeof this.el_.webkitEnterFullScreen === 'function') { 19961 var userAgent = window_1.navigator && window_1.navigator.userAgent || ''; 19962 19963 // Seems to be broken in Chromium/Chrome && Safari in Leopard 19964 if (/Android/.test(userAgent) || !/Chrome|Mac OS X 10.5/.test(userAgent)) { 19965 return true; 19966 } 19967 } 19968 return false; 19969 }; 19970 19971 /** 19972 * Request that the `HTML5` Tech enter fullscreen. 19973 */ 19974 19975 19976 Html5.prototype.enterFullScreen = function enterFullScreen() { 19977 var video = this.el_; 19978 19979 if (video.paused && video.networkState <= video.HAVE_METADATA) { 19980 // attempt to prime the video element for programmatic access 19981 // this isn't necessary on the desktop but shouldn't hurt 19982 this.el_.play(); 19983 19984 // playing and pausing synchronously during the transition to fullscreen 19985 // can get iOS ~6.1 devices into a play/pause loop 19986 this.setTimeout(function () { 19987 video.pause(); 19988 video.webkitEnterFullScreen(); 19989 }, 0); 19990 } else { 19991 video.webkitEnterFullScreen(); 19992 } 19993 }; 19994 19995 /** 19996 * Request that the `HTML5` Tech exit fullscreen. 19997 */ 19998 19999 20000 Html5.prototype.exitFullScreen = function exitFullScreen() { 20001 this.el_.webkitExitFullScreen(); 20002 }; 20003 20004 /** 20005 * A getter/setter for the `Html5` Tech's source object. 20006 * > Note: Please use {@link Html5#setSource} 20007 * 20008 * @param {Tech~SourceObject} [src] 20009 * The source object you want to set on the `HTML5` techs element. 20010 * 20011 * @return {Tech~SourceObject|undefined} 20012 * - The current source object when a source is not passed in. 20013 * - undefined when setting 20014 * 20015 * @deprecated Since version 5. 20016 */ 20017 20018 20019 Html5.prototype.src = function src(_src) { 20020 if (_src === undefined) { 20021 return this.el_.src; 20022 } 20023 20024 // Setting src through `src` instead of `setSrc` will be deprecated 20025 this.setSrc(_src); 20026 }; 20027 20028 /** 20029 * Reset the tech by removing all sources and then calling 20030 * {@link Html5.resetMediaElement}. 20031 */ 20032 20033 20034 Html5.prototype.reset = function reset() { 20035 Html5.resetMediaElement(this.el_); 20036 }; 20037 20038 /** 20039 * Get the current source on the `HTML5` Tech. Falls back to returning the source from 20040 * the HTML5 media element. 20041 * 20042 * @return {Tech~SourceObject} 20043 * The current source object from the HTML5 tech. With a fallback to the 20044 * elements source. 20045 */ 20046 20047 20048 Html5.prototype.currentSrc = function currentSrc() { 20049 if (this.currentSource_) { 20050 return this.currentSource_.src; 20051 } 20052 return this.el_.currentSrc; 20053 }; 20054 20055 /** 20056 * Set controls attribute for the HTML5 media Element. 20057 * 20058 * @param {string} val 20059 * Value to set the controls attribute to 20060 */ 20061 20062 20063 Html5.prototype.setControls = function setControls(val) { 20064 this.el_.controls = !!val; 20065 }; 20066 20067 /** 20068 * Create and returns a remote {@link TextTrack} object. 20069 * 20070 * @param {string} kind 20071 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 20072 * 20073 * @param {string} [label] 20074 * Label to identify the text track 20075 * 20076 * @param {string} [language] 20077 * Two letter language abbreviation 20078 * 20079 * @return {TextTrack} 20080 * The TextTrack that gets created. 20081 */ 20082 20083 20084 Html5.prototype.addTextTrack = function addTextTrack(kind, label, language) { 20085 if (!this.featuresNativeTextTracks) { 20086 return _Tech.prototype.addTextTrack.call(this, kind, label, language); 20087 } 20088 20089 return this.el_.addTextTrack(kind, label, language); 20090 }; 20091 20092 /** 20093 * Creates either native TextTrack or an emulated TextTrack depending 20094 * on the value of `featuresNativeTextTracks` 20095 * 20096 * @param {Object} options 20097 * The object should contain the options to initialize the TextTrack with. 20098 * 20099 * @param {string} [options.kind] 20100 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 20101 * 20102 * @param {string} [options.label] 20103 * Label to identify the text track 20104 * 20105 * @param {string} [options.language] 20106 * Two letter language abbreviation. 20107 * 20108 * @param {boolean} [options.default] 20109 * Default this track to on. 20110 * 20111 * @param {string} [options.id] 20112 * The internal id to assign this track. 20113 * 20114 * @param {string} [options.src] 20115 * A source url for the track. 20116 * 20117 * @return {HTMLTrackElement} 20118 * The track element that gets created. 20119 */ 20120 20121 20122 Html5.prototype.createRemoteTextTrack = function createRemoteTextTrack(options) { 20123 if (!this.featuresNativeTextTracks) { 20124 return _Tech.prototype.createRemoteTextTrack.call(this, options); 20125 } 20126 var htmlTrackElement = document_1.createElement('track'); 20127 20128 if (options.kind) { 20129 htmlTrackElement.kind = options.kind; 20130 } 20131 if (options.label) { 20132 htmlTrackElement.label = options.label; 20133 } 20134 if (options.language || options.srclang) { 20135 htmlTrackElement.srclang = options.language || options.srclang; 20136 } 20137 if (options.default) { 20138 htmlTrackElement.default = options.default; 20139 } 20140 if (options.id) { 20141 htmlTrackElement.id = options.id; 20142 } 20143 if (options.src) { 20144 htmlTrackElement.src = options.src; 20145 } 20146 20147 return htmlTrackElement; 20148 }; 20149 20150 /** 20151 * Creates a remote text track object and returns an html track element. 20152 * 20153 * @param {Object} options The object should contain values for 20154 * kind, language, label, and src (location of the WebVTT file) 20155 * @param {Boolean} [manualCleanup=true] if set to false, the TextTrack will be 20156 * automatically removed from the video element whenever the source changes 20157 * @return {HTMLTrackElement} An Html Track Element. 20158 * This can be an emulated {@link HTMLTrackElement} or a native one. 20159 * @deprecated The default value of the "manualCleanup" parameter will default 20160 * to "false" in upcoming versions of Video.js 20161 */ 20162 20163 20164 Html5.prototype.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 20165 var htmlTrackElement = _Tech.prototype.addRemoteTextTrack.call(this, options, manualCleanup); 20166 20167 if (this.featuresNativeTextTracks) { 20168 this.el().appendChild(htmlTrackElement); 20169 } 20170 20171 return htmlTrackElement; 20172 }; 20173 20174 /** 20175 * Remove remote `TextTrack` from `TextTrackList` object 20176 * 20177 * @param {TextTrack} track 20178 * `TextTrack` object to remove 20179 */ 20180 20181 20182 Html5.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { 20183 _Tech.prototype.removeRemoteTextTrack.call(this, track); 20184 20185 if (this.featuresNativeTextTracks) { 20186 var tracks = this.$$('track'); 20187 20188 var i = tracks.length; 20189 20190 while (i--) { 20191 if (track === tracks[i] || track === tracks[i].track) { 20192 this.el().removeChild(tracks[i]); 20193 } 20194 } 20195 } 20196 }; 20197 20198 /** 20199 * Gets available media playback quality metrics as specified by the W3C's Media 20200 * Playback Quality API. 20201 * 20202 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 20203 * 20204 * @return {Object} 20205 * An object with supported media playback quality metrics 20206 */ 20207 20208 20209 Html5.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 20210 if (typeof this.el().getVideoPlaybackQuality === 'function') { 20211 return this.el().getVideoPlaybackQuality(); 20212 } 20213 20214 var videoPlaybackQuality = {}; 20215 20216 if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') { 20217 videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount; 20218 videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount; 20219 } 20220 20221 if (window_1.performance && typeof window_1.performance.now === 'function') { 20222 videoPlaybackQuality.creationTime = window_1.performance.now(); 20223 } else if (window_1.performance && window_1.performance.timing && typeof window_1.performance.timing.navigationStart === 'number') { 20224 videoPlaybackQuality.creationTime = window_1.Date.now() - window_1.performance.timing.navigationStart; 20225 } 20226 20227 return videoPlaybackQuality; 20228 }; 20229 20230 return Html5; 20231 }(Tech); 20232 20233 /* HTML5 Support Testing ---------------------------------------------------- */ 20234 20235 if (isReal()) { 20236 20237 /** 20238 * Element for testing browser HTML5 media capabilities 20239 * 20240 * @type {Element} 20241 * @constant 20242 * @private 20243 */ 20244 Html5.TEST_VID = document_1.createElement('video'); 20245 var track = document_1.createElement('track'); 20246 20247 track.kind = 'captions'; 20248 track.srclang = 'en'; 20249 track.label = 'English'; 20250 Html5.TEST_VID.appendChild(track); 20251 } 20252 20253 /** 20254 * Check if HTML5 media is supported by this browser/device. 20255 * 20256 * @return {boolean} 20257 * - True if HTML5 media is supported. 20258 * - False if HTML5 media is not supported. 20259 */ 20260 Html5.isSupported = function () { 20261 // IE with no Media Player is a LIAR! (#984) 20262 try { 20263 Html5.TEST_VID.volume = 0.5; 20264 } catch (e) { 20265 return false; 20266 } 20267 20268 return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType); 20269 }; 20270 20271 /** 20272 * Check if the tech can support the given type 20273 * 20274 * @param {string} type 20275 * The mimetype to check 20276 * @return {string} 'probably', 'maybe', or '' (empty string) 20277 */ 20278 Html5.canPlayType = function (type) { 20279 return Html5.TEST_VID.canPlayType(type); 20280 }; 20281 20282 /** 20283 * Check if the tech can support the given source 20284 * @param {Object} srcObj 20285 * The source object 20286 * @param {Object} options 20287 * The options passed to the tech 20288 * @return {string} 'probably', 'maybe', or '' (empty string) 20289 */ 20290 Html5.canPlaySource = function (srcObj, options) { 20291 return Html5.canPlayType(srcObj.type); 20292 }; 20293 20294 /** 20295 * Check if the volume can be changed in this browser/device. 20296 * Volume cannot be changed in a lot of mobile devices. 20297 * Specifically, it can't be changed from 1 on iOS. 20298 * 20299 * @return {boolean} 20300 * - True if volume can be controlled 20301 * - False otherwise 20302 */ 20303 Html5.canControlVolume = function () { 20304 // IE will error if Windows Media Player not installed #3315 20305 try { 20306 var volume = Html5.TEST_VID.volume; 20307 20308 Html5.TEST_VID.volume = volume / 2 + 0.1; 20309 return volume !== Html5.TEST_VID.volume; 20310 } catch (e) { 20311 return false; 20312 } 20313 }; 20314 20315 /** 20316 * Check if the playback rate can be changed in this browser/device. 20317 * 20318 * @return {boolean} 20319 * - True if playback rate can be controlled 20320 * - False otherwise 20321 */ 20322 Html5.canControlPlaybackRate = function () { 20323 // Playback rate API is implemented in Android Chrome, but doesn't do anything 20324 // https://github.com/videojs/video.js/issues/3180 20325 if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) { 20326 return false; 20327 } 20328 // IE will error if Windows Media Player not installed #3315 20329 try { 20330 var playbackRate = Html5.TEST_VID.playbackRate; 20331 20332 Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1; 20333 return playbackRate !== Html5.TEST_VID.playbackRate; 20334 } catch (e) { 20335 return false; 20336 } 20337 }; 20338 20339 /** 20340 * Check if we can override a video/audio elements attributes, with 20341 * Object.defineProperty. 20342 * 20343 * @return {boolean} 20344 * - True if builtin attributes can be overridden 20345 * - False otherwise 20346 */ 20347 Html5.canOverrideAttributes = function () { 20348 // if we cannot overwrite the src/innerHTML property, there is no support 20349 // iOS 7 safari for instance cannot do this. 20350 try { 20351 var noop = function noop() {}; 20352 20353 Object.defineProperty(document_1.createElement('video'), 'src', { get: noop, set: noop }); 20354 Object.defineProperty(document_1.createElement('audio'), 'src', { get: noop, set: noop }); 20355 Object.defineProperty(document_1.createElement('video'), 'innerHTML', { get: noop, set: noop }); 20356 Object.defineProperty(document_1.createElement('audio'), 'innerHTML', { get: noop, set: noop }); 20357 } catch (e) { 20358 return false; 20359 } 20360 20361 return true; 20362 }; 20363 20364 /** 20365 * Check to see if native `TextTrack`s are supported by this browser/device. 20366 * 20367 * @return {boolean} 20368 * - True if native `TextTrack`s are supported. 20369 * - False otherwise 20370 */ 20371 Html5.supportsNativeTextTracks = function () { 20372 return IS_ANY_SAFARI; 20373 }; 20374 20375 /** 20376 * Check to see if native `VideoTrack`s are supported by this browser/device 20377 * 20378 * @return {boolean} 20379 * - True if native `VideoTrack`s are supported. 20380 * - False otherwise 20381 */ 20382 Html5.supportsNativeVideoTracks = function () { 20383 return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks); 20384 }; 20385 20386 /** 20387 * Check to see if native `AudioTrack`s are supported by this browser/device 20388 * 20389 * @return {boolean} 20390 * - True if native `AudioTrack`s are supported. 20391 * - False otherwise 20392 */ 20393 Html5.supportsNativeAudioTracks = function () { 20394 return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks); 20395 }; 20396 20397 /** 20398 * An array of events available on the Html5 tech. 20399 * 20400 * @private 20401 * @type {Array} 20402 */ 20403 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']; 20404 20405 /** 20406 * Boolean indicating whether the `Tech` supports volume control. 20407 * 20408 * @type {boolean} 20409 * @default {@link Html5.canControlVolume} 20410 */ 20411 Html5.prototype.featuresVolumeControl = Html5.canControlVolume(); 20412 20413 /** 20414 * Boolean indicating whether the `Tech` supports changing the speed at which the media 20415 * plays. Examples: 20416 * - Set player to play 2x (twice) as fast 20417 * - Set player to play 0.5x (half) as fast 20418 * 20419 * @type {boolean} 20420 * @default {@link Html5.canControlPlaybackRate} 20421 */ 20422 Html5.prototype.featuresPlaybackRate = Html5.canControlPlaybackRate(); 20423 20424 /** 20425 * Boolean indicating whether the `Tech` supports the `sourceset` event. 20426 * 20427 * @type {boolean} 20428 * @default 20429 */ 20430 Html5.prototype.featuresSourceset = Html5.canOverrideAttributes(); 20431 20432 /** 20433 * Boolean indicating whether the `HTML5` tech currently supports the media element 20434 * moving in the DOM. iOS breaks if you move the media element, so this is set this to 20435 * false there. Everywhere else this should be true. 20436 * 20437 * @type {boolean} 20438 * @default 20439 */ 20440 Html5.prototype.movingMediaElementInDOM = !IS_IOS; 20441 20442 // TODO: Previous comment: No longer appears to be used. Can probably be removed. 20443 // Is this true? 20444 /** 20445 * Boolean indicating whether the `HTML5` tech currently supports automatic media resize 20446 * when going into fullscreen. 20447 * 20448 * @type {boolean} 20449 * @default 20450 */ 20451 Html5.prototype.featuresFullscreenResize = true; 20452 20453 /** 20454 * Boolean indicating whether the `HTML5` tech currently supports the progress event. 20455 * If this is false, manual `progress` events will be triggered instead. 20456 * 20457 * @type {boolean} 20458 * @default 20459 */ 20460 Html5.prototype.featuresProgressEvents = true; 20461 20462 /** 20463 * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event. 20464 * If this is false, manual `timeupdate` events will be triggered instead. 20465 * 20466 * @default 20467 */ 20468 Html5.prototype.featuresTimeupdateEvents = true; 20469 20470 /** 20471 * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s. 20472 * 20473 * @type {boolean} 20474 * @default {@link Html5.supportsNativeTextTracks} 20475 */ 20476 Html5.prototype.featuresNativeTextTracks = Html5.supportsNativeTextTracks(); 20477 20478 /** 20479 * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s. 20480 * 20481 * @type {boolean} 20482 * @default {@link Html5.supportsNativeVideoTracks} 20483 */ 20484 Html5.prototype.featuresNativeVideoTracks = Html5.supportsNativeVideoTracks(); 20485 20486 /** 20487 * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s. 20488 * 20489 * @type {boolean} 20490 * @default {@link Html5.supportsNativeAudioTracks} 20491 */ 20492 Html5.prototype.featuresNativeAudioTracks = Html5.supportsNativeAudioTracks(); 20493 20494 // HTML5 Feature detection and Device Fixes --------------------------------- // 20495 var canPlayType = Html5.TEST_VID && Html5.TEST_VID.constructor.prototype.canPlayType; 20496 var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; 20497 20498 Html5.patchCanPlayType = function () { 20499 20500 // Android 4.0 and above can play HLS to some extent but it reports being unable to do so 20501 // Firefox and Chrome report correctly 20502 if (ANDROID_VERSION >= 4.0 && !IS_FIREFOX && !IS_CHROME) { 20503 Html5.TEST_VID.constructor.prototype.canPlayType = function (type) { 20504 if (type && mpegurlRE.test(type)) { 20505 return 'maybe'; 20506 } 20507 return canPlayType.call(this, type); 20508 }; 20509 } 20510 }; 20511 20512 Html5.unpatchCanPlayType = function () { 20513 var r = Html5.TEST_VID.constructor.prototype.canPlayType; 20514 20515 Html5.TEST_VID.constructor.prototype.canPlayType = canPlayType; 20516 return r; 20517 }; 20518 20519 // by default, patch the media element 20520 Html5.patchCanPlayType(); 20521 20522 Html5.disposeMediaElement = function (el) { 20523 if (!el) { 20524 return; 20525 } 20526 20527 if (el.parentNode) { 20528 el.parentNode.removeChild(el); 20529 } 20530 20531 // remove any child track or source nodes to prevent their loading 20532 while (el.hasChildNodes()) { 20533 el.removeChild(el.firstChild); 20534 } 20535 20536 // remove any src reference. not setting `src=''` because that causes a warning 20537 // in firefox 20538 el.removeAttribute('src'); 20539 20540 // force the media element to update its loading state by calling load() 20541 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 20542 if (typeof el.load === 'function') { 20543 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 20544 (function () { 20545 try { 20546 el.load(); 20547 } catch (e) { 20548 // not supported 20549 } 20550 })(); 20551 } 20552 }; 20553 20554 Html5.resetMediaElement = function (el) { 20555 if (!el) { 20556 return; 20557 } 20558 20559 var sources = el.querySelectorAll('source'); 20560 var i = sources.length; 20561 20562 while (i--) { 20563 el.removeChild(sources[i]); 20564 } 20565 20566 // remove any src reference. 20567 // not setting `src=''` because that throws an error 20568 el.removeAttribute('src'); 20569 20570 if (typeof el.load === 'function') { 20571 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 20572 (function () { 20573 try { 20574 el.load(); 20575 } catch (e) { 20576 // satisfy linter 20577 } 20578 })(); 20579 } 20580 }; 20581 20582 /* Native HTML5 element property wrapping ----------------------------------- */ 20583 // Wrap native boolean attributes with getters that check both property and attribute 20584 // The list is as followed: 20585 // muted, defaultMuted, autoplay, controls, loop, playsinline 20586 [ 20587 /** 20588 * Get the value of `muted` from the media element. `muted` indicates 20589 * that the volume for the media should be set to silent. This does not actually change 20590 * the `volume` attribute. 20591 * 20592 * @method Html5#muted 20593 * @return {boolean} 20594 * - True if the value of `volume` should be ignored and the audio set to silent. 20595 * - False if the value of `volume` should be used. 20596 * 20597 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 20598 */ 20599 'muted', 20600 20601 /** 20602 * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates 20603 * whether the media should start muted or not. Only changes the default state of the 20604 * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the 20605 * current state. 20606 * 20607 * @method Html5#defaultMuted 20608 * @return {boolean} 20609 * - The value of `defaultMuted` from the media element. 20610 * - True indicates that the media should start muted. 20611 * - False indicates that the media should not start muted 20612 * 20613 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 20614 */ 20615 'defaultMuted', 20616 20617 /** 20618 * Get the value of `autoplay` from the media element. `autoplay` indicates 20619 * that the media should start to play as soon as the page is ready. 20620 * 20621 * @method Html5#autoplay 20622 * @return {boolean} 20623 * - The value of `autoplay` from the media element. 20624 * - True indicates that the media should start as soon as the page loads. 20625 * - False indicates that the media should not start as soon as the page loads. 20626 * 20627 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 20628 */ 20629 'autoplay', 20630 20631 /** 20632 * Get the value of `controls` from the media element. `controls` indicates 20633 * whether the native media controls should be shown or hidden. 20634 * 20635 * @method Html5#controls 20636 * @return {boolean} 20637 * - The value of `controls` from the media element. 20638 * - True indicates that native controls should be showing. 20639 * - False indicates that native controls should be hidden. 20640 * 20641 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls} 20642 */ 20643 'controls', 20644 20645 /** 20646 * Get the value of `loop` from the media element. `loop` indicates 20647 * that the media should return to the start of the media and continue playing once 20648 * it reaches the end. 20649 * 20650 * @method Html5#loop 20651 * @return {boolean} 20652 * - The value of `loop` from the media element. 20653 * - True indicates that playback should seek back to start once 20654 * the end of a media is reached. 20655 * - False indicates that playback should not loop back to the start when the 20656 * end of the media is reached. 20657 * 20658 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 20659 */ 20660 'loop', 20661 20662 /** 20663 * Get the value of `playsinline` from the media element. `playsinline` indicates 20664 * to the browser that non-fullscreen playback is preferred when fullscreen 20665 * playback is the native default, such as in iOS Safari. 20666 * 20667 * @method Html5#playsinline 20668 * @return {boolean} 20669 * - The value of `playsinline` from the media element. 20670 * - True indicates that the media should play inline. 20671 * - False indicates that the media should not play inline. 20672 * 20673 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 20674 */ 20675 'playsinline'].forEach(function (prop) { 20676 Html5.prototype[prop] = function () { 20677 return this.el_[prop] || this.el_.hasAttribute(prop); 20678 }; 20679 }); 20680 20681 // Wrap native boolean attributes with setters that set both property and attribute 20682 // The list is as followed: 20683 // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline 20684 // setControls is special-cased above 20685 [ 20686 /** 20687 * Set the value of `muted` on the media element. `muted` indicates that the current 20688 * audio level should be silent. 20689 * 20690 * @method Html5#setMuted 20691 * @param {boolean} muted 20692 * - True if the audio should be set to silent 20693 * - False otherwise 20694 * 20695 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 20696 */ 20697 'muted', 20698 20699 /** 20700 * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current 20701 * audio level should be silent, but will only effect the muted level on intial playback.. 20702 * 20703 * @method Html5.prototype.setDefaultMuted 20704 * @param {boolean} defaultMuted 20705 * - True if the audio should be set to silent 20706 * - False otherwise 20707 * 20708 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 20709 */ 20710 'defaultMuted', 20711 20712 /** 20713 * Set the value of `autoplay` on the media element. `autoplay` indicates 20714 * that the media should start to play as soon as the page is ready. 20715 * 20716 * @method Html5#setAutoplay 20717 * @param {boolean} autoplay 20718 * - True indicates that the media should start as soon as the page loads. 20719 * - False indicates that the media should not start as soon as the page loads. 20720 * 20721 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 20722 */ 20723 'autoplay', 20724 20725 /** 20726 * Set the value of `loop` on the media element. `loop` indicates 20727 * that the media should return to the start of the media and continue playing once 20728 * it reaches the end. 20729 * 20730 * @method Html5#setLoop 20731 * @param {boolean} loop 20732 * - True indicates that playback should seek back to start once 20733 * the end of a media is reached. 20734 * - False indicates that playback should not loop back to the start when the 20735 * end of the media is reached. 20736 * 20737 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 20738 */ 20739 'loop', 20740 20741 /** 20742 * Set the value of `playsinline` from the media element. `playsinline` indicates 20743 * to the browser that non-fullscreen playback is preferred when fullscreen 20744 * playback is the native default, such as in iOS Safari. 20745 * 20746 * @method Html5#setPlaysinline 20747 * @param {boolean} playsinline 20748 * - True indicates that the media should play inline. 20749 * - False indicates that the media should not play inline. 20750 * 20751 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 20752 */ 20753 'playsinline'].forEach(function (prop) { 20754 Html5.prototype['set' + toTitleCase(prop)] = function (v) { 20755 this.el_[prop] = v; 20756 20757 if (v) { 20758 this.el_.setAttribute(prop, prop); 20759 } else { 20760 this.el_.removeAttribute(prop); 20761 } 20762 }; 20763 }); 20764 20765 // Wrap native properties with a getter 20766 // The list is as followed 20767 // paused, currentTime, buffered, volume, poster, preload, error, seeking 20768 // seekable, ended, playbackRate, defaultPlaybackRate, played, networkState 20769 // readyState, videoWidth, videoHeight 20770 [ 20771 /** 20772 * Get the value of `paused` from the media element. `paused` indicates whether the media element 20773 * is currently paused or not. 20774 * 20775 * @method Html5#paused 20776 * @return {boolean} 20777 * The value of `paused` from the media element. 20778 * 20779 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused} 20780 */ 20781 'paused', 20782 20783 /** 20784 * Get the value of `currentTime` from the media element. `currentTime` indicates 20785 * the current second that the media is at in playback. 20786 * 20787 * @method Html5#currentTime 20788 * @return {number} 20789 * The value of `currentTime` from the media element. 20790 * 20791 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime} 20792 */ 20793 'currentTime', 20794 20795 /** 20796 * Get the value of `buffered` from the media element. `buffered` is a `TimeRange` 20797 * object that represents the parts of the media that are already downloaded and 20798 * available for playback. 20799 * 20800 * @method Html5#buffered 20801 * @return {TimeRange} 20802 * The value of `buffered` from the media element. 20803 * 20804 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered} 20805 */ 20806 'buffered', 20807 20808 /** 20809 * Get the value of `volume` from the media element. `volume` indicates 20810 * the current playback volume of audio for a media. `volume` will be a value from 0 20811 * (silent) to 1 (loudest and default). 20812 * 20813 * @method Html5#volume 20814 * @return {number} 20815 * The value of `volume` from the media element. Value will be between 0-1. 20816 * 20817 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 20818 */ 20819 'volume', 20820 20821 /** 20822 * Get the value of `poster` from the media element. `poster` indicates 20823 * that the url of an image file that can/will be shown when no media data is available. 20824 * 20825 * @method Html5#poster 20826 * @return {string} 20827 * The value of `poster` from the media element. Value will be a url to an 20828 * image. 20829 * 20830 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster} 20831 */ 20832 'poster', 20833 20834 /** 20835 * Get the value of `preload` from the media element. `preload` indicates 20836 * what should download before the media is interacted with. It can have the following 20837 * values: 20838 * - none: nothing should be downloaded 20839 * - metadata: poster and the first few frames of the media may be downloaded to get 20840 * media dimensions and other metadata 20841 * - auto: allow the media and metadata for the media to be downloaded before 20842 * interaction 20843 * 20844 * @method Html5#preload 20845 * @return {string} 20846 * The value of `preload` from the media element. Will be 'none', 'metadata', 20847 * or 'auto'. 20848 * 20849 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 20850 */ 20851 'preload', 20852 20853 /** 20854 * Get the value of the `error` from the media element. `error` indicates any 20855 * MediaError that may have occurred during playback. If error returns null there is no 20856 * current error. 20857 * 20858 * @method Html5#error 20859 * @return {MediaError|null} 20860 * The value of `error` from the media element. Will be `MediaError` if there 20861 * is a current error and null otherwise. 20862 * 20863 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error} 20864 */ 20865 'error', 20866 20867 /** 20868 * Get the value of `seeking` from the media element. `seeking` indicates whether the 20869 * media is currently seeking to a new position or not. 20870 * 20871 * @method Html5#seeking 20872 * @return {boolean} 20873 * - The value of `seeking` from the media element. 20874 * - True indicates that the media is currently seeking to a new position. 20875 * - False indicates that the media is not seeking to a new position at this time. 20876 * 20877 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking} 20878 */ 20879 'seeking', 20880 20881 /** 20882 * Get the value of `seekable` from the media element. `seekable` returns a 20883 * `TimeRange` object indicating ranges of time that can currently be `seeked` to. 20884 * 20885 * @method Html5#seekable 20886 * @return {TimeRange} 20887 * The value of `seekable` from the media element. A `TimeRange` object 20888 * indicating the current ranges of time that can be seeked to. 20889 * 20890 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable} 20891 */ 20892 'seekable', 20893 20894 /** 20895 * Get the value of `ended` from the media element. `ended` indicates whether 20896 * the media has reached the end or not. 20897 * 20898 * @method Html5#ended 20899 * @return {boolean} 20900 * - The value of `ended` from the media element. 20901 * - True indicates that the media has ended. 20902 * - False indicates that the media has not ended. 20903 * 20904 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended} 20905 */ 20906 'ended', 20907 20908 /** 20909 * Get the value of `playbackRate` from the media element. `playbackRate` indicates
20910 * the rate at which the media is currently playing back. Examples: 20911 * - if playbackRate is set to 2, media will play twice as fast. 20912 * - if playbackRate is set to 0.5, media will play half as fast. 20913 * 20914 * @method Html5#playbackRate 20915 * @return {number} 20916 * The value of `playbackRate` from the media element. A number indicating 20917 * the current playback speed of the media, where 1 is normal speed. 20918 * 20919 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 20920 */ 20921 'playbackRate', 20922 20923 /** 20924 * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates 20925 * the rate at which the media is currently playing back. This value will not indicate the current 20926 * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that. 20927 * 20928 * Examples: 20929 * - if defaultPlaybackRate is set to 2, media will play twice as fast. 20930 * - if defaultPlaybackRate is set to 0.5, media will play half as fast. 20931 * 20932 * @method Html5.prototype.defaultPlaybackRate 20933 * @return {number} 20934 * The value of `defaultPlaybackRate` from the media element. A number indicating 20935 * the current playback speed of the media, where 1 is normal speed. 20936 * 20937 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 20938 */ 20939 'defaultPlaybackRate', 20940 20941 /** 20942 * Get the value of `played` from the media element. `played` returns a `TimeRange` 20943 * object representing points in the media timeline that have been played. 20944 * 20945 * @method Html5#played 20946 * @return {TimeRange} 20947 * The value of `played` from the media element. A `TimeRange` object indicating 20948 * the ranges of time that have been played. 20949 * 20950 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played} 20951 */ 20952 'played', 20953 20954 /** 20955 * Get the value of `networkState` from the media element. `networkState` indicates 20956 * the current network state. It returns an enumeration from the following list: 20957 * - 0: NETWORK_EMPTY 20958 * - 1: NETWORK_IDLE 20959 * - 2: NETWORK_LOADING 20960 * - 3: NETWORK_NO_SOURCE 20961 * 20962 * @method Html5#networkState 20963 * @return {number} 20964 * The value of `networkState` from the media element. This will be a number 20965 * from the list in the description. 20966 * 20967 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} 20968 */ 20969 'networkState', 20970 20971 /** 20972 * Get the value of `readyState` from the media element. `readyState` indicates 20973 * the current state of the media element. It returns an enumeration from the 20974 * following list: 20975 * - 0: HAVE_NOTHING 20976 * - 1: HAVE_METADATA 20977 * - 2: HAVE_CURRENT_DATA 20978 * - 3: HAVE_FUTURE_DATA 20979 * - 4: HAVE_ENOUGH_DATA 20980 * 20981 * @method Html5#readyState 20982 * @return {number} 20983 * The value of `readyState` from the media element. This will be a number 20984 * from the list in the description. 20985 * 20986 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} 20987 */ 20988 'readyState', 20989 20990 /** 20991 * Get the value of `videoWidth` from the video element. `videoWidth` indicates 20992 * the current width of the video in css pixels. 20993 * 20994 * @method Html5#videoWidth 20995 * @return {number} 20996 * The value of `videoWidth` from the video element. This will be a number 20997 * in css pixels. 20998 * 20999 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 21000 */ 21001 'videoWidth', 21002 21003 /** 21004 * Get the value of `videoHeight` from the video element. `videoHeight` indicates 21005 * the current height of the video in css pixels. 21006 * 21007 * @method Html5#videoHeight 21008 * @return {number} 21009 * The value of `videoHeight` from the video element. This will be a number 21010 * in css pixels. 21011 * 21012 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 21013 */ 21014 'videoHeight'].forEach(function (prop) { 21015 Html5.prototype[prop] = function () { 21016 return this.el_[prop]; 21017 }; 21018 }); 21019 21020 // Wrap native properties with a setter in this format: 21021 // set + toTitleCase(name) 21022 // The list is as follows: 21023 // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate 21024 [ 21025 /** 21026 * Set the value of `volume` on the media element. `volume` indicates the current 21027 * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and 21028 * so on. 21029 * 21030 * @method Html5#setVolume 21031 * @param {number} percentAsDecimal 21032 * The volume percent as a decimal. Valid range is from 0-1. 21033 * 21034 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 21035 */ 21036 'volume', 21037 21038 /** 21039 * Set the value of `src` on the media element. `src` indicates the current 21040 * {@link Tech~SourceObject} for the media. 21041 * 21042 * @method Html5#setSrc 21043 * @param {Tech~SourceObject} src 21044 * The source object to set as the current source. 21045 * 21046 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src} 21047 */ 21048 'src', 21049 21050 /** 21051 * Set the value of `poster` on the media element. `poster` is the url to 21052 * an image file that can/will be shown when no media data is available. 21053 * 21054 * @method Html5#setPoster 21055 * @param {string} poster 21056 * The url to an image that should be used as the `poster` for the media 21057 * element. 21058 * 21059 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster} 21060 */ 21061 'poster', 21062 21063 /** 21064 * Set the value of `preload` on the media element. `preload` indicates 21065 * what should download before the media is interacted with. It can have the following 21066 * values: 21067 * - none: nothing should be downloaded 21068 * - metadata: poster and the first few frames of the media may be downloaded to get 21069 * media dimensions and other metadata 21070 * - auto: allow the media and metadata for the media to be downloaded before 21071 * interaction 21072 * 21073 * @method Html5#setPreload 21074 * @param {string} preload 21075 * The value of `preload` to set on the media element. Must be 'none', 'metadata', 21076 * or 'auto'. 21077 * 21078 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 21079 */ 21080 'preload', 21081 21082 /** 21083 * Set the value of `playbackRate` on the media element. `playbackRate` indicates 21084 * the rate at which the media should play back. Examples: 21085 * - if playbackRate is set to 2, media will play twice as fast. 21086 * - if playbackRate is set to 0.5, media will play half as fast. 21087 * 21088 * @method Html5#setPlaybackRate 21089 * @return {number} 21090 * The value of `playbackRate` from the media element. A number indicating 21091 * the current playback speed of the media, where 1 is normal speed. 21092 * 21093 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 21094 */ 21095 'playbackRate', 21096 21097 /** 21098 * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates 21099 * the rate at which the media should play back upon initial startup. Changing this value 21100 * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}. 21101 * 21102 * Example Values: 21103 * - if playbackRate is set to 2, media will play twice as fast. 21104 * - if playbackRate is set to 0.5, media will play half as fast. 21105 * 21106 * @method Html5.prototype.setDefaultPlaybackRate 21107 * @return {number} 21108 * The value of `defaultPlaybackRate` from the media element. A number indicating 21109 * the current playback speed of the media, where 1 is normal speed. 21110 * 21111 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate} 21112 */ 21113 'defaultPlaybackRate'].forEach(function (prop) { 21114 Html5.prototype['set' + toTitleCase(prop)] = function (v) { 21115 this.el_[prop] = v; 21116 }; 21117 }); 21118 21119 // wrap native functions with a function 21120 // The list is as follows: 21121 // pause, load, play 21122 [ 21123 /** 21124 * A wrapper around the media elements `pause` function. This will call the `HTML5` 21125 * media elements `pause` function. 21126 * 21127 * @method Html5#pause 21128 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause} 21129 */ 21130 'pause', 21131 21132 /** 21133 * A wrapper around the media elements `load` function. This will call the `HTML5`s 21134 * media element `load` function. 21135 * 21136 * @method Html5#load 21137 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load} 21138 */ 21139 'load', 21140 21141 /** 21142 * A wrapper around the media elements `play` function. This will call the `HTML5`s 21143 * media element `play` function. 21144 * 21145 * @method Html5#play 21146 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play} 21147 */ 21148 'play'].forEach(function (prop) { 21149 Html5.prototype[prop] = function () { 21150 return this.el_[prop](); 21151 }; 21152 }); 21153 21154 Tech.withSourceHandlers(Html5); 21155 21156 /** 21157 * Native source handler for Html5, simply passes the source to the media element. 21158 * 21159 * @property {Tech~SourceObject} source 21160 * The source object 21161 * 21162 * @property {Html5} tech 21163 * The instance of the HTML5 tech. 21164 */ 21165 Html5.nativeSourceHandler = {}; 21166 21167 /** 21168 * Check if the media element can play the given mime type. 21169 * 21170 * @param {string} type 21171 * The mimetype to check 21172 * 21173 * @return {string} 21174 * 'probably', 'maybe', or '' (empty string) 21175 */ 21176 Html5.nativeSourceHandler.canPlayType = function (type) { 21177 // IE without MediaPlayer throws an error (#519) 21178 try {
vendor: 1,664 bytes, lines 21179-21239
21179 return Html5.TEST_VID.canPlayType(type); 21180 } catch (e) { 21181 return ''; 21182 } 21183 }; 21184 21185 /** 21186 * Check if the media element can handle a source natively. 21187 * 21188 * @param {Tech~SourceObject} source 21189 * The source object 21190 * 21191 * @param {Object} [options] 21192 * Options to be passed to the tech. 21193 * 21194 * @return {string} 21195 * 'probably', 'maybe', or '' (empty string). 21196 */ 21197 Html5.nativeSourceHandler.canHandleSource = function (source, options) { 21198 21199 // If a type was provided we should rely on that 21200 if (source.type) { 21201 return Html5.nativeSourceHandler.canPlayType(source.type); 21202 21203 // If no type, fall back to checking 'video/[EXTENSION]' 21204 } else if (source.src) { 21205 var ext = getFileExtension(source.src); 21206 21207 return Html5.nativeSourceHandler.canPlayType('video/' + ext); 21208 } 21209 21210 return ''; 21211 }; 21212 21213 /** 21214 * Pass the source to the native media element. 21215 * 21216 * @param {Tech~SourceObject} source 21217 * The source object 21218 * 21219 * @param {Html5} tech 21220 * The instance of the Html5 tech 21221 * 21222 * @param {Object} [options] 21223 * The options to pass to the source 21224 */ 21225 Html5.nativeSourceHandler.handleSource = function (source, tech, options) { 21226 tech.setSrc(source.src); 21227 }; 21228 21229 /** 21230 * A noop for the native dispose function, as cleanup is not needed. 21231 */ 21232 Html5.nativeSourceHandler.dispose = function () {}; 21233 21234 // Register the native source handler 21235 Html5.registerSourceHandler(Html5.nativeSourceHandler); 21236 21237 Tech.registerTech('Html5', Html5); 21238 21239 var _templateObject$2 = taggedTemplateLiteralLoose(['\n Using the tech directly can be dangerous. I hope you know
21239what 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 ']); 21240 21241 // The following tech events are simply re-triggered 21242 // on the player when they happen 21243 var TECH_EVENTS_RETRIGGER = [ 21244 /** 21245 * Fired while the user agent is downloading media data. 21246 * 21247 * @event Player#progress 21248 * @type {EventTarget~Event} 21249 */ 21250 /** 21251 * Retrigger the `progress` event that was triggered by the {@link Tech}. 21252 * 21253 * @private 21254 * @method Player#handleTechProgress_ 21255 * @fires Player#progress 21256 * @listens Tech#progress 21257 */ 21258 'progress', 21259 21260 /** 21261 * Fires when the loading of an audio/video is aborted. 21262 * 21263 * @event Player#abort 21264 * @type {EventTarget~Event} 21265 */ 21266 /** 21267 * Retrigger the `abort` event that was triggered by the {@link Tech}. 21268 * 21269 * @private 21270 * @method Player#handleTechAbort_ 21271 * @fires Player#abort 21272 * @listens Tech#abort 21273 */ 21274 'abort', 21275 21276 /** 21277 * Fires when the browser is intentionally not getting media data. 21278 * 21279 * @event Player#suspend 21280 * @type {EventTarget~Event} 21281 */ 21282 /** 21283 * Retrigger the `suspend` event that was triggered by the {@link Tech}. 21284 * 21285 * @private 21286 * @method Player#handleTechSuspend_ 21287 * @fires Player#suspend 21288 * @listens Tech#suspend 21289 */ 21290 'suspend', 21291 21292 /** 21293 * Fires when the current playlist is empty. 21294 * 21295 * @event Player#emptied 21296 * @type {EventTarget~Event} 21297 */ 21298 /** 21299 * Retrigger the `emptied` event that was triggered by the {@link Tech}. 21300 * 21301 * @private 21302 * @method Player#handleTechEmptied_ 21303 * @fires Player#emptied 21304 * @listens Tech#emptied 21305 */ 21306 'emptied', 21307 /** 21308 * Fires when the browser is trying to get media data, but data is not available. 21309 * 21310 * @event Player#stalled 21311 * @type {EventTarget~Event} 21312 */ 21313 /** 21314 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 21315 * 21316 * @private 21317 * @method Player#handleTechStalled_ 21318 * @fires Player#stalled 21319 * @listens Tech#stalled 21320 */ 21321 'stalled', 21322 21323 /** 21324 * Fires when the browser has loaded meta data for the audio/video. 21325 * 21326 * @event Player#loadedmetadata 21327 * @type {EventTarget~Event} 21328 */ 21329 /** 21330 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 21331 * 21332 * @private 21333 * @method Player#handleTechLoadedmetadata_ 21334 * @fires Player#loadedmetadata 21335 * @listens Tech#loadedmetadata 21336 */ 21337 'loadedmetadata', 21338 21339 /** 21340 * Fires when the browser has loaded the current frame of the audio/video. 21341 * 21342 * @event Player#loadeddata 21343 * @type {event} 21344 */ 21345 /** 21346 * Retrigger the `loadeddata` event that was triggered by the {@link Tech}. 21347 * 21348 * @private 21349 * @method Player#handleTechLoaddeddata_ 21350 * @fires Player#loadeddata 21351 * @listens Tech#loadeddata 21352 */ 21353 'loadeddata', 21354 21355 /** 21356 * Fires when the current playback position has changed. 21357 * 21358 * @event Player#timeupdate 21359 * @type {event} 21360 */ 21361 /** 21362 * Retrigger the `timeupdate` event that was triggered by the {@link Tech}. 21363 * 21364 * @private 21365 * @method Player#handleTechTimeUpdate_ 21366 * @fires Player#timeupdate 21367 * @listens Tech#timeupdate 21368 */ 21369 'timeupdate', 21370 21371 /** 21372 * Fires when the video's intrinsic dimensions change 21373 * 21374 * @event Player#resize 21375 * @type {event} 21376 */ 21377 /** 21378 * Retrigger the `resize` event that was triggered by the {@link Tech}. 21379 * 21380 * @private 21381 * @method Player#handleTechResize_ 21382 * @fires Player#resize 21383 * @listens Tech#resize 21384 */ 21385 'resize', 21386 21387 /** 21388 * Fires when the volume has been changed 21389 * 21390 * @event Player#volumechange 21391 * @type {event} 21392 */ 21393 /** 21394 * Retrigger the `volumechange` event that was triggered by the {@link Tech}. 21395 * 21396 * @private 21397 * @method Player#handleTechVolumechange_ 21398 * @fires Player#volumechange 21399 * @listens Tech#volumechange 21400 */ 21401 'volumechange', 21402 21403 /** 21404 * Fires when the text track has been changed 21405 * 21406 * @event Player#texttrackchange 21407 * @type {event} 21408 */ 21409 /** 21410 * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}. 21411 * 21412 * @private 21413 * @method Player#handleTechTexttrackchange_ 21414 * @fires Player#texttrackchange 21415 * @listens Tech#texttrackchange 21416 */ 21417 'texttrackchange']; 21418 21419 // events to queue when playback rate is zero 21420 // this is a hash for the sole purpose of mapping non-camel-cased event names 21421 // to camel-cased function names 21422 var TECH_EVENTS_QUEUE = { 21423 canplay: 'CanPlay', 21424 canplaythrough: 'CanPlayThrough', 21425 playing: 'Playing', 21426 seeked: 'Seeked' 21427 }; 21428 21429 /** 21430 * An instance of the `Player` class is created when any of the Video.js setup methods 21431 * are used to initialize a video. 21432 * 21433 * After an instance has been created it can be accessed globally in two ways: 21434 * 1. By calling `videojs('example_video_1');` 21435 * 2. By using it directly via `videojs.players.example_video_1;` 21436 * 21437 * @extends Component 21438 */ 21439 21440 var Player = function (_Component) { 21441 inherits(Player, _Component); 21442 21443 /** 21444 * Create an instance of this class. 21445 * 21446 * @param {Element} tag
vendor: 35,174 bytes, lines 21447-22490
21447 * The original video DOM element used for configuring options. 21448 * 21449 * @param {Object} [options] 21450 * Object of option names and values. 21451 * 21452 * @param {Component~ReadyCallback} [ready] 21453 * Ready callback function. 21454 */ 21455 function Player(tag, options, ready) { 21456 classCallCheck(this, Player); 21457 21458 // Make sure tag ID exists 21459 tag.id = tag.id || options.id || 'vjs_video_' + newGUID(); 21460 21461 // Set Options 21462 // The options argument overrides options set in the video tag 21463 // which overrides globally set options. 21464 // This latter part coincides with the load order 21465 // (tag must exist before Player) 21466 options = assign(Player.getTagSettings(tag), options); 21467 21468 // Delay the initialization of children because we need to set up 21469 // player properties first, and can't use `this` before `super()` 21470 options.initChildren = false; 21471 21472 // Same with creating the element 21473 options.createEl = false; 21474 21475 // don't auto mixin the evented mixin 21476 options.evented = false; 21477 21478 // we don't want the player to report touch activity on itself 21479 // see enableTouchActivity in Component 21480 options.reportTouchActivity = false; 21481 21482 // If language is not set, get the closest lang attribute 21483 if (!options.language) { 21484 if (typeof tag.closest === 'function') { 21485 var closest = tag.closest('[lang]'); 21486 21487 if (closest && closest.getAttribute) { 21488 options.language = closest.getAttribute('lang'); 21489 } 21490 } else { 21491 var element = tag; 21492 21493 while (element && element.nodeType === 1) { 21494 if (getAttributes(element).hasOwnProperty('lang')) { 21495 options.language = element.getAttribute('lang'); 21496 break; 21497 } 21498 element = element.parentNode; 21499 } 21500 } 21501 } 21502 21503 // Run base component initializing with new options 21504 21505 // Tracks when a tech changes the poster 21506 var _this = possibleConstructorReturn(this, _Component.call(this, null, options, ready)); 21507 21508 _this.isPosterFromTech_ = false; 21509 21510 // Holds callback info that gets queued when playback rate is zero 21511 // and a seek is happening 21512 _this.queuedCallbacks_ = []; 21513 21514 // Turn off API access because we're loading a new tech that might load asynchronously 21515 _this.isReady_ = false; 21516 21517 // Init state hasStarted_ 21518 _this.hasStarted_ = false; 21519 21520 // Init state userActive_ 21521 _this.userActive_ = false; 21522 21523 // if the global option object was accidentally blown away by 21524 // someone, bail early with an informative error 21525 if (!_this.options_ || !_this.options_.techOrder || !_this.options_.techOrder.length) { 21526 throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?'); 21527 } 21528 21529 // Store the original tag used to set options 21530 _this.tag = tag; 21531 21532 // Store the tag attributes used to restore html5 element 21533 _this.tagAttributes = tag && getAttributes(tag); 21534 21535 // Update current language 21536 _this.language(_this.options_.language); 21537 21538 // Update Supported Languages 21539 if (options.languages) { 21540 // Normalise player option languages to lowercase 21541 var languagesToLower = {}; 21542 21543 Object.getOwnPropertyNames(options.languages).forEach(function (name$$1) { 21544 languagesToLower[name$$1.toLowerCase()] = options.languages[name$$1]; 21545 }); 21546 _this.languages_ = languagesToLower; 21547 } else { 21548 _this.languages_ = Player.prototype.options_.languages; 21549 } 21550 21551 // Cache for video property values. 21552 _this.cache_ = {}; 21553 21554 // Set poster 21555 _this.poster_ = options.poster || ''; 21556 21557 // Set controls 21558 _this.controls_ = !!options.controls; 21559 21560 // Set default values for lastVolume 21561 _this.cache_.lastVolume = 1; 21562 21563 // Original tag settings stored in options 21564 // now remove immediately so native controls don't flash. 21565 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 21566 tag.controls = false; 21567 tag.removeAttribute('controls'); 21568 21569 /* 21570 * Store the internal state of scrubbing 21571 * 21572 * @private 21573 * @return {Boolean} True if the user is scrubbing 21574 */ 21575 _this.scrubbing_ = false; 21576 21577 _this.el_ = _this.createEl(); 21578 21579 // Set default value for lastPlaybackRate 21580 _this.cache_.lastPlaybackRate = _this.defaultPlaybackRate(); 21581 21582 // Make this an evented object and use `el_` as its event bus. 21583 evented(_this, { eventBusKey: 'el_' }); 21584 21585 // We also want to pass the original player options to each component and plugin 21586 // as well so they don't need to reach back into the player for options later. 21587 // We also need to do another copy of this.options_ so we don't end up with 21588 // an infinite loop. 21589 var playerOptionsCopy = mergeOptions(_this.options_); 21590 21591 // Load plugins 21592 if (options.plugins) { 21593 var plugins = options.plugins; 21594 21595 Object.keys(plugins).forEach(function (name$$1) { 21596 if (typeof this[name$$1] === 'function') { 21597 this[name$$1](plugins[name$$1]); 21598 } else { 21599 throw new Error('plugin "' + name$$1 + '" does not exist'); 21600 } 21601 }, _this); 21602 } 21603 21604 _this.options_.playerOptions = playerOptionsCopy; 21605 21606 _this.middleware_ = []; 21607 21608 _this.initChildren(); 21609 21610 // Set isAudio based on whether or not an audio tag was used 21611 _this.isAudio(tag.nodeName.toLowerCase() === 'audio'); 21612 21613 // Update controls className. Can't do this when the controls are initially 21614 // set because the element doesn't exist yet. 21615 if (_this.controls()) { 21616 _this.addClass('vjs-controls-enabled'); 21617 } else { 21618 _this.addClass('vjs-controls-disabled'); 21619 } 21620 21621 // Set ARIA label and region role depending on player type 21622 _this.el_.setAttribute('role', 'region'); 21623 if (_this.isAudio()) { 21624 _this.el_.setAttribute('aria-label', _this.localize('Audio Player')); 21625 } else { 21626 _this.el_.setAttribute('aria-label', _this.localize('Video Player')); 21627 } 21628 21629 if (_this.isAudio()) { 21630 _this.addClass('vjs-audio'); 21631 } 21632 21633 if (_this.flexNotSupported_()) { 21634 _this.addClass('vjs-no-flex'); 21635 } 21636 21637 // TODO: Make this smarter. Toggle user state between touching/mousing 21638 // using events, since devices can have both touch and mouse events. 21639 // if (browser.TOUCH_ENABLED) { 21640 // this.addClass('vjs-touch-enabled'); 21641 // } 21642 21643 // iOS Safari has broken hover handling 21644 if (!IS_IOS) { 21645 _this.addClass('vjs-workinghover'); 21646 } 21647 21648 // Make player easily findable by ID 21649 Player.players[_this.id_] = _this; 21650 21651 // Add a major version class to aid css in plugins 21652 var majorVersion = version.split('.')[0]; 21653 21654 _this.addClass('vjs-v' + majorVersion); 21655 21656 // When the player is first initialized, trigger activity so components 21657 // like the control bar show themselves if needed 21658 _this.userActive(true); 21659 _this.reportUserActivity(); 21660 21661 _this.one('play', _this.listenForUserActivity_); 21662 _this.on('fullscreenchange', _this.handleFullscreenChange_); 21663 _this.on('stageclick', _this.handleStageClick_); 21664 21665 _this.changingSrc_ = false; 21666 _this.playWaitingForReady_ = false; 21667 _this.playOnLoadstart_ = null; 21668 return _this; 21669 } 21670 21671 /** 21672 * Destroys the video player and does any necessary cleanup. 21673 * 21674 * This is especially helpful if you are dynamically adding and removing videos 21675 * to/from the DOM. 21676 * 21677 * @fires Player#dispose 21678 */ 21679 21680 21681 Player.prototype.dispose = function dispose() { 21682 /** 21683 * Called when the player is being disposed of. 21684 * 21685 * @event Player#dispose 21686 * @type {EventTarget~Event} 21687 */ 21688 this.trigger('dispose'); 21689 // prevent dispose from being called twice 21690 this.off('dispose'); 21691 21692 if (this.styleEl_ && this.styleEl_.parentNode) { 21693 this.styleEl_.parentNode.removeChild(this.styleEl_); 21694 this.styleEl_ = null; 21695 } 21696 21697 // Kill reference to this player 21698 Player.players[this.id_] = null; 21699 21700 if (this.tag && this.tag.player) { 21701 this.tag.player = null; 21702 } 21703 21704 if (this.el_ && this.el_.player) { 21705 this.el_.player = null; 21706 } 21707 21708 if (this.tech_) { 21709 this.tech_.dispose(); 21710 this.isPosterFromTech_ = false; 21711 this.poster_ = ''; 21712 } 21713 21714 if (this.playerElIngest_) { 21715 this.playerElIngest_ = null; 21716 } 21717 21718 if (this.tag) { 21719 this.tag = null; 21720 } 21721 21722 clearCacheForPlayer(this); 21723 21724 // the actual .el_ is removed here 21725 _Component.prototype.dispose.call(this); 21726 }; 21727 21728 /** 21729 * Create the `Player`'s DOM element. 21730 * 21731 * @return {Element} 21732 * The DOM element that gets created. 21733 */ 21734 21735 21736 Player.prototype.createEl = function createEl$$1() { 21737 var tag = this.tag; 21738 var el = void 0; 21739 var playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player'); 21740 var divEmbed = this.tag.tagName.toLowerCase() === 'video-js'; 21741 21742 if (playerElIngest) { 21743 el = this.el_ = tag.parentNode; 21744 } else if (!divEmbed) { 21745 el = this.el_ = _Component.prototype.createEl.call(this, 'div'); 21746 } 21747 21748 // Copy over all the attributes from the tag, including ID and class 21749 // ID will now reference player box, not the video tag 21750 var attrs = getAttributes(tag); 21751 21752 if (divEmbed) { 21753 el = this.el_ = tag; 21754 tag = this.tag = document_1.createElement('video'); 21755 while (el.children.length) { 21756 tag.appendChild(el.firstChild); 21757 } 21758 21759 if (!hasClass(el, 'video-js')) { 21760 addClass(el, 'video-js'); 21761 } 21762 21763 el.appendChild(tag); 21764 21765 playerElIngest = this.playerElIngest_ = el; 21766 // move properties over from our custom `video-js` element 21767 // to our new `video` element. This will move things like 21768 // `src` or `controls` that were set via js before the player 21769 // was initialized. 21770 Object.keys(el).forEach(function (k) { 21771 tag[k] = el[k]; 21772 }); 21773 } 21774 21775 // set tabindex to -1 to remove the video element from the focus order 21776 tag.setAttribute('tabindex', '-1'); 21777 // Workaround for #4583 (JAWS+IE doesn't announce BPB or play button) 21778 // See https://github.com/FreedomScientific/VFO-standards-support/issues/78 21779 // Note that we can't detect if JAWS is being used, but this ARIA attribute 21780 // doesn't change behavior of IE11 if JAWS is not being used 21781 if (IE_VERSION) { 21782 tag.setAttribute('role', 'application'); 21783 } 21784 21785 // Remove width/height attrs from tag so CSS can make it 100% width/height 21786 tag.removeAttribute('width'); 21787 tag.removeAttribute('height'); 21788 21789 Object.getOwnPropertyNames(attrs).forEach(function (attr) { 21790 // don't copy over the class attribute to the player element when we're in a div embed 21791 // the class is already set up properly in the divEmbed case 21792 // and we want to make sure that the `video-js` class doesn't get lost 21793 if (!(divEmbed && attr === 'class')) { 21794 el.setAttribute(attr, attrs[attr]); 21795 } 21796 21797 if (divEmbed) { 21798 tag.setAttribute(attr, attrs[attr]); 21799 } 21800 }); 21801 21802 // Update tag id/class for use as HTML5 playback tech 21803 // Might think we should do this after embedding in container so .vjs-tech class 21804 // doesn't flash 100% width/height, but class only applies with .video-js parent 21805 tag.playerId = tag.id; 21806 tag.id += '_html5_api'; 21807 tag.className = 'vjs-tech'; 21808 21809 // Make player findable on elements 21810 tag.player = el.player = this; 21811 // Default state of video is paused 21812 this.addClass('vjs-paused'); 21813 21814 // Add a style element in the player that we'll use to set the width/height 21815 // of the player in a way that's still overrideable by CSS, just like the 21816 // video element 21817 if (window_1.VIDEOJS_NO_DYNAMIC_STYLE !== true) { 21818 this.styleEl_ = createStyleElement('vjs-styles-dimensions'); 21819 var defaultsStyleEl = $('.vjs-styles-defaults'); 21820 var head = $('head'); 21821 21822 head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild); 21823 } 21824 21825 // Pass in the width/height/aspectRatio options which will update the style el 21826 this.width(this.options_.width); 21827 this.height(this.options_.height); 21828 this.fluid(this.options_.fluid); 21829 this.aspectRatio(this.options_.aspectRatio); 21830 21831 // Hide any links within the video/audio tag, 21832 // because IE doesn't hide them completely from screen readers. 21833 var links = tag.getElementsByTagName('a'); 21834 21835 for (var i = 0; i < links.length; i++) { 21836 var linkEl = links.item(i); 21837 21838 addClass(linkEl, 'vjs-hidden'); 21839 linkEl.setAttribute('hidden', 'hidden'); 21840 } 21841 21842 // insertElFirst seems to cause the networkState to flicker from 3 to 2, so 21843 // keep track of the original for later so we can know if the source originally failed 21844 tag.initNetworkState_ = tag.networkState; 21845 21846 // Wrap video tag in div (el/box) container 21847 if (tag.parentNode && !playerElIngest) { 21848 tag.parentNode.insertBefore(el, tag); 21849 } 21850 21851 // insert the tag as the first child of the player element 21852 // then manually add it to the children array so that this.addChild 21853 // will work properly for other components 21854 // 21855 // Breaks iPhone, fixed in HTML5 setup. 21856 prependTo(tag, el); 21857 this.children_.unshift(tag); 21858 21859 // Set lang attr on player to ensure CSS :lang() in consistent with player 21860 // if it's been set to something different to the doc 21861 this.el_.setAttribute('lang', this.language_); 21862 21863 this.el_ = el; 21864 21865 return el; 21866 }; 21867 21868 /** 21869 * A getter/setter for the `Player`'s width. Returns the player's configured value. 21870 * To get the current width use `currentWidth()`. 21871 * 21872 * @param {number} [value] 21873 * The value to set the `Player`'s width to. 21874 * 21875 * @return {number} 21876 * The current width of the `Player` when getting. 21877 */ 21878 21879 21880 Player.prototype.width = function width(value) { 21881 return this.dimension('width', value); 21882 }; 21883 21884 /** 21885 * A getter/setter for the `Player`'s height. Returns the player's configured value. 21886 * To get the current height use `currentheight()`. 21887 * 21888 * @param {number} [value] 21889 * The value to set the `Player`'s heigth to. 21890 * 21891 * @return {number} 21892 * The current height of the `Player` when getting. 21893 */ 21894 21895 21896 Player.prototype.height = function height(value) { 21897 return this.dimension('height', value); 21898 }; 21899 21900 /** 21901 * A getter/setter for the `Player`'s width & height. 21902 * 21903 * @param {string} dimension 21904 * This string can be: 21905 * - 'width' 21906 * - 'height' 21907 * 21908 * @param {number} [value] 21909 * Value for dimension specified in the first argument. 21910 * 21911 * @return {number} 21912 * The dimension arguments value when getting (width/height). 21913 */ 21914 21915 21916 Player.prototype.dimension = function dimension(_dimension, value) { 21917 var privDimension = _dimension + '_'; 21918 21919 if (value === undefined) { 21920 return this[privDimension] || 0; 21921 } 21922 21923 if (value === '') { 21924 // If an empty string is given, reset the dimension to be automatic 21925 this[privDimension] = undefined; 21926 this.updateStyleEl_(); 21927 return; 21928 } 21929 21930 var parsedVal = parseFloat(value); 21931 21932 if (isNaN(parsedVal)) { 21933 log$1.error('Improper value "' + value + '" supplied for for ' + _dimension); 21934 return; 21935 } 21936 21937 this[privDimension] = parsedVal; 21938 this.updateStyleEl_(); 21939 }; 21940 21941 /** 21942 * A getter/setter/toggler for the vjs-fluid `className` on the `Player`. 21943 * 21944 * @param {boolean} [bool] 21945 * - A value of true adds the class. 21946 * - A value of false removes the class. 21947 * - No value will toggle the fluid class. 21948 * 21949 * @return {boolean|undefined} 21950 * - The value of fluid when getting. 21951 * - `undefined` when setting. 21952 */ 21953 21954 21955 Player.prototype.fluid = function fluid(bool) { 21956 if (bool === undefined) { 21957 return !!this.fluid_; 21958 } 21959 21960 this.fluid_ = !!bool; 21961 21962 if (bool) { 21963 this.addClass('vjs-fluid'); 21964 } else { 21965 this.removeClass('vjs-fluid'); 21966 } 21967 21968 this.updateStyleEl_(); 21969 }; 21970 21971 /** 21972 * Get/Set the aspect ratio 21973 * 21974 * @param {string} [ratio] 21975 * Aspect ratio for player 21976 * 21977 * @return {string|undefined} 21978 * returns the current aspect ratio when getting 21979 */ 21980 21981 /** 21982 * A getter/setter for the `Player`'s aspect ratio. 21983 * 21984 * @param {string} [ratio] 21985 * The value to set the `Player's aspect ratio to. 21986 * 21987 * @return {string|undefined} 21988 * - The current aspect ratio of the `Player` when getting. 21989 * - undefined when setting 21990 */ 21991 21992 21993 Player.prototype.aspectRatio = function aspectRatio(ratio) { 21994 if (ratio === undefined) { 21995 return this.aspectRatio_; 21996 } 21997 21998 // Check for width:height format 21999 if (!/^\d+\:\d+$/.test(ratio)) { 22000 throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.'); 22001 } 22002 this.aspectRatio_ = ratio; 22003 22004 // We're assuming if you set an aspect ratio you want fluid mode, 22005 // because in fixed mode you could calculate width and height yourself. 22006 this.fluid(true); 22007 22008 this.updateStyleEl_(); 22009 }; 22010 22011 /** 22012 * Update styles of the `Player` element (height, width and aspect ratio). 22013 * 22014 * @private 22015 * @listens Tech#loadedmetadata 22016 */ 22017 22018 22019 Player.prototype.updateStyleEl_ = function updateStyleEl_() { 22020 if (window_1.VIDEOJS_NO_DYNAMIC_STYLE === true) { 22021 var _width = typeof this.width_ === 'number' ? this.width_ : this.options_.width; 22022 var _height = typeof this.height_ === 'number' ? this.height_ : this.options_.height; 22023 var techEl = this.tech_ && this.tech_.el(); 22024 22025 if (techEl) { 22026 if (_width >= 0) { 22027 techEl.width = _width; 22028 } 22029 if (_height >= 0) { 22030 techEl.height = _height; 22031 } 22032 } 22033 22034 return; 22035 } 22036 22037 var width = void 0; 22038 var height = void 0; 22039 var aspectRatio = void 0; 22040 var idClass = void 0; 22041 22042 // The aspect ratio is either used directly or to calculate width and height. 22043 if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { 22044 // Use any aspectRatio that's been specifically set 22045 aspectRatio = this.aspectRatio_; 22046 } else if (this.videoWidth() > 0) { 22047 // Otherwise try to get the aspect ratio from the video metadata 22048 aspectRatio = this.videoWidth() + ':' + this.videoHeight(); 22049 } else { 22050 // Or use a default. The video element's is 2:1, but 16:9 is more common. 22051 aspectRatio = '16:9'; 22052 } 22053 22054 // Get the ratio as a decimal we can use to calculate dimensions 22055 var ratioParts = aspectRatio.split(':'); 22056 var ratioMultiplier = ratioParts[1] / ratioParts[0]; 22057 22058 if (this.width_ !== undefined) { 22059 // Use any width that's been specifically set 22060 width = this.width_; 22061 } else if (this.height_ !== undefined) { 22062 // Or calulate the width from the aspect ratio if a height has been set 22063 width = this.height_ / ratioMultiplier; 22064 } else { 22065 // Or use the video's metadata, or use the video el's default of 300 22066 width = this.videoWidth() || 300; 22067 } 22068 22069 if (this.height_ !== undefined) { 22070 // Use any height that's been specifically set 22071 height = this.height_; 22072 } else { 22073 // Otherwise calculate the height from the ratio and the width 22074 height = width * ratioMultiplier; 22075 } 22076 22077 // Ensure the CSS class is valid by starting with an alpha character 22078 if (/^[^a-zA-Z]/.test(this.id())) { 22079 idClass = 'dimensions-' + this.id(); 22080 } else { 22081 idClass = this.id() + '-dimensions'; 22082 } 22083 22084 // Ensure the right class is still on the player for the style element 22085 this.addClass(idClass); 22086 22087 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 '); 22088 }; 22089 22090 /** 22091 * Load/Create an instance of playback {@link Tech} including element 22092 * and API methods. Then append the `Tech` element in `Player` as a child. 22093 * 22094 * @param {string} techName 22095 * name of the playback technology 22096 * 22097 * @param {string} source 22098 * video source 22099 * 22100 * @private 22101 */ 22102 22103 22104 Player.prototype.loadTech_ = function loadTech_(techName, source) { 22105 var _this2 = this; 22106 22107 // Pause and remove current playback technology 22108 if (this.tech_) { 22109 this.unloadTech_(); 22110 } 22111 22112 var titleTechName = toTitleCase(techName); 22113 var camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1); 22114 22115 // get rid of the HTML5 video tag as soon as we are using another tech 22116 if (titleTechName !== 'Html5' && this.tag) { 22117 Tech.getTech('Html5').disposeMediaElement(this.tag); 22118 this.tag.player = null; 22119 this.tag = null; 22120 } 22121 22122 this.techName_ = titleTechName; 22123 22124 // Turn off API access because we're loading a new tech that might load asynchronously 22125 this.isReady_ = false; 22126 22127 // Grab tech-specific options from player options and add source and parent element to use. 22128 var techOptions = { 22129 source: source, 22130 'nativeControlsForTouch': this.options_.nativeControlsForTouch, 22131 'playerId': this.id(), 22132 'techId': this.id() + '_' + titleTechName + '_api', 22133 'autoplay': this.options_.autoplay, 22134 'playsinline': this.options_.playsinline, 22135 'preload': this.options_.preload, 22136 'loop': this.options_.loop, 22137 'muted': this.options_.muted, 22138 'poster': this.poster(), 22139 'language': this.language(), 22140 'playerElIngest': this.playerElIngest_ || false, 22141 'vtt.js': this.options_['vtt.js'], 22142 'canOverridePoster': !!this.options_.techCanOverridePoster, 22143 'enableSourceset': this.options_.enableSourceset 22144 }; 22145 22146 ALL.names.forEach(function (name$$1) { 22147 var props = ALL[name$$1]; 22148 22149 techOptions[props.getterName] = _this2[props.privateName]; 22150 }); 22151 22152 assign(techOptions, this.options_[titleTechName]); 22153 assign(techOptions, this.options_[camelTechName]); 22154 assign(techOptions, this.options_[techName.toLowerCase()]); 22155 22156 if (this.tag) { 22157 techOptions.tag = this.tag; 22158 } 22159 22160 if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { 22161 techOptions.startTime = this.cache_.currentTime; 22162 } 22163 22164 // Initialize tech instance 22165 var TechClass = Tech.getTech(techName); 22166 22167 if (!TechClass) { 22168 throw new Error('No Tech named \'' + titleTechName + '\' exists! \'' + titleTechName + '\' should be registered using videojs.registerTech()\''); 22169 } 22170 22171 this.tech_ = new TechClass(techOptions); 22172 22173 // player.triggerReady is always async, so don't need this to be async 22174 this.tech_.ready(bind(this, this.handleTechReady_), true); 22175 22176 textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_); 22177 22178 // Listen to all HTML5-defined events and trigger them on the player 22179 TECH_EVENTS_RETRIGGER.forEach(function (event) { 22180 _this2.on(_this2.tech_, event, _this2['handleTech' + toTitleCase(event) + '_']); 22181 }); 22182 22183 Object.keys(TECH_EVENTS_QUEUE).forEach(function (event) { 22184 _this2.on(_this2.tech_, event, function (eventObj) { 22185 if (_this2.tech_.playbackRate() === 0 && _this2.tech_.seeking()) { 22186 _this2.queuedCallbacks_.push({ 22187 callback: _this2['handleTech' + TECH_EVENTS_QUEUE[event] + '_'].bind(_this2), 22188 event: eventObj 22189 }); 22190 return; 22191 } 22192 _this2['handleTech' + TECH_EVENTS_QUEUE[event] + '_'](eventObj); 22193 }); 22194 }); 22195 22196 this.on(this.tech_, 'loadstart', this.handleTechLoadStart_); 22197 this.on(this.tech_, 'sourceset', this.handleTechSourceset_); 22198 this.on(this.tech_, 'waiting', this.handleTechWaiting_); 22199 this.on(this.tech_, 'ended', this.handleTechEnded_); 22200 this.on(this.tech_, 'seeking', this.handleTechSeeking_); 22201 this.on(this.tech_, 'play', this.handleTechPlay_); 22202 this.on(this.tech_, 'firstplay', this.handleTechFirstPlay_); 22203 this.on(this.tech_, 'pause', this.handleTechPause_); 22204 this.on(this.tech_, 'durationchange', this.handleTechDurationChange_); 22205 this.on(this.tech_, 'fullscreenchange', this.handleTechFullscreenChange_); 22206 this.on(this.tech_, 'error', this.handleTechError_); 22207 this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_); 22208 this.on(this.tech_, 'posterchange', this.handleTechPosterChange_); 22209 this.on(this.tech_, 'textdata', this.handleTechTextData_); 22210 this.on(this.tech_, 'ratechange', this.handleTechRateChange_); 22211 22212 this.usingNativeControls(this.techGet_('controls')); 22213 22214 if (this.controls() && !this.usingNativeControls()) { 22215 this.addTechControlsListeners_(); 22216 } 22217 22218 // Add the tech element in the DOM if it was not already there 22219 // Make sure to not insert the original video element if using Html5 22220 if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) { 22221 prependTo(this.tech_.el(), this.el()); 22222 } 22223 22224 // Get rid of the original video tag reference after the first tech is loaded 22225 if (this.tag) { 22226 this.tag.player = null; 22227 this.tag = null; 22228 } 22229 }; 22230 22231 /** 22232 * Unload and dispose of the current playback {@link Tech}. 22233 * 22234 * @private 22235 */ 22236 22237 22238 Player.prototype.unloadTech_ = function unloadTech_() { 22239 var _this3 = this; 22240 22241 // Save the current text tracks so that we can reuse the same text tracks with the next tech 22242 ALL.names.forEach(function (name$$1) { 22243 var props = ALL[name$$1]; 22244 22245 _this3[props.privateName] = _this3[props.getterName](); 22246 }); 22247 this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); 22248 22249 this.isReady_ = false; 22250 22251 this.tech_.dispose(); 22252 22253 this.tech_ = false; 22254 22255 if (this.isPosterFromTech_) { 22256 this.poster_ = ''; 22257 this.trigger('posterchange'); 22258 } 22259 22260 this.isPosterFromTech_ = false; 22261 }; 22262 22263 /** 22264 * Return a reference to the current {@link Tech}. 22265 * It will print a warning by default about the danger of using the tech directly 22266 * but any argument that is passed in will silence the warning. 22267 * 22268 * @param {*} [safety] 22269 * Anything passed in to silence the warning 22270 * 22271 * @return {Tech} 22272 * The Tech 22273 */ 22274 22275 22276 Player.prototype.tech = function tech(safety) { 22277 if (safety === undefined) { 22278 log$1.warn(tsml(_templateObject$2)); 22279 } 22280 22281 return this.tech_; 22282 }; 22283 22284 /** 22285 * Set up click and touch listeners for the playback element 22286 * 22287 * - On desktops: a click on the video itself will toggle playback 22288 * - On mobile devices: a click on the video toggles controls 22289 * which is done by toggling the user state between active and 22290 * inactive 22291 * - A tap can signal that a user has become active or has become inactive 22292 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 22293 * quick tap should hide them again (signaling the user is in an inactive 22294 * viewing state) 22295 * - In addition to this, we still want the user to be considered inactive after 22296 * a few seconds of inactivity. 22297 * 22298 * > Note: the only part of iOS interaction we can't mimic with this setup 22299 * is a touch and hold on the video element counting as activity in order to 22300 * keep the controls showing, but that shouldn't be an issue. A touch and hold 22301 * on any controls will still keep the user active 22302 * 22303 * @private 22304 */ 22305 22306 22307 Player.prototype.addTechControlsListeners_ = function addTechControlsListeners_() { 22308 // Make sure to remove all the previous listeners in case we are called multiple times. 22309 this.removeTechControlsListeners_(); 22310 22311 // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do 22312 // trigger mousedown/up. 22313 // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object 22314 // Any touch events are set to block the mousedown event from happening 22315 this.on(this.tech_, 'mousedown', this.handleTechClick_); 22316 22317 // If the controls were hidden we don't want that to change without a tap event 22318 // so we'll check if the controls were already showing before reporting user 22319 // activity 22320 this.on(this.tech_, 'touchstart', this.handleTechTouchStart_); 22321 this.on(this.tech_, 'touchmove', this.handleTechTouchMove_); 22322 this.on(this.tech_, 'touchend', this.handleTechTouchEnd_); 22323 22324 // The tap listener needs to come after the touchend listener because the tap 22325 // listener cancels out any reportedUserActivity when setting userActive(false) 22326 this.on(this.tech_, 'tap', this.handleTechTap_); 22327 }; 22328 22329 /** 22330 * Remove the listeners used for click and tap controls. This is needed for 22331 * toggling to controls disabled, where a tap/touch should do nothing. 22332 * 22333 * @private 22334 */ 22335 22336 22337 Player.prototype.removeTechControlsListeners_ = function removeTechControlsListeners_() { 22338 // We don't want to just use `this.off()` because there might be other needed 22339 // listeners added by techs that extend this. 22340 this.off(this.tech_, 'tap', this.handleTechTap_); 22341 this.off(this.tech_, 'touchstart', this.handleTechTouchStart_); 22342 this.off(this.tech_, 'touchmove', this.handleTechTouchMove_); 22343 this.off(this.tech_, 'touchend', this.handleTechTouchEnd_); 22344 this.off(this.tech_, 'mousedown', this.handleTechClick_); 22345 }; 22346 22347 /** 22348 * Player waits for the tech to be ready 22349 * 22350 * @private 22351 */ 22352 22353 22354 Player.prototype.handleTechReady_ = function handleTechReady_() { 22355 this.triggerReady(); 22356 22357 // Keep the same volume as before 22358 if (this.cache_.volume) { 22359 this.techCall_('setVolume', this.cache_.volume); 22360 } 22361 22362 // Look if the tech found a higher resolution poster while loading 22363 this.handleTechPosterChange_(); 22364 22365 // Update the duration if available 22366 this.handleTechDurationChange_(); 22367 22368 // Chrome and Safari both have issues with autoplay. 22369 // In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work. 22370 // In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays) 22371 // This fixes both issues. Need to wait for API, so it updates displays correctly 22372 if ((this.src() || this.currentSrc()) && this.tag && this.options_.autoplay && this.paused()) { 22373 try { 22374 // Chrome Fix. Fixed in Chrome v16. 22375 delete this.tag.poster; 22376 } catch (e) { 22377 log$1('deleting tag.poster throws in some browsers', e); 22378 } 22379 } 22380 }; 22381 22382 /** 22383 * Retrigger the `loadstart` event that was triggered by the {@link Tech}. This 22384 * function will also trigger {@link Player#firstplay} if it is the first loadstart 22385 * for a video. 22386 * 22387 * @fires Player#loadstart 22388 * @fires Player#firstplay 22389 * @listens Tech#loadstart 22390 * @private 22391 */ 22392 22393 22394 Player.prototype.handleTechLoadStart_ = function handleTechLoadStart_() { 22395 // TODO: Update to use `emptied` event instead. See #1277. 22396 22397 this.removeClass('vjs-ended'); 22398 this.removeClass('vjs-seeking'); 22399 22400 // reset the error state 22401 this.error(null); 22402 22403 // If it's already playing we want to trigger a firstplay event now. 22404 // The firstplay event relies on both the play and loadstart events 22405 // which can happen in any order for a new source 22406 if (!this.paused()) { 22407 /** 22408 * Fired when the user agent begins looking for media data 22409 * 22410 * @event Player#loadstart 22411 * @type {EventTarget~Event} 22412 */ 22413 this.trigger('loadstart'); 22414 this.trigger('firstplay'); 22415 } else { 22416 // reset the hasStarted state 22417 this.hasStarted(false); 22418 this.trigger('loadstart'); 22419 } 22420 }; 22421 22422 /** 22423 * Update the internal source caches so that we return the correct source from 22424 * `src()`, `currentSource()`, and `currentSources()`. 22425 * 22426 * > Note: `currentSources` will not be updated if the source that is passed in exists 22427 * in the current `currentSources` cache. 22428 * 22429 * 22430 * @param {Tech~SourceObject} srcObj 22431 * A string or object source to update our caches to. 22432 */ 22433 22434 22435 Player.prototype.updateSourceCaches_ = function updateSourceCaches_() { 22436 var srcObj = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 22437 22438 22439 var src = srcObj; 22440 var type = ''; 22441 22442 if (typeof src !== 'string') { 22443 src = srcObj.src; 22444 type = srcObj.type; 22445 } 22446 // make sure all the caches are set to default values 22447 // to prevent null checking 22448 this.cache_.source = this.cache_.source || {}; 22449 this.cache_.sources = this.cache_.sources || []; 22450 22451 // try to get the type of the src that was passed in 22452 if (src && !type) { 22453 type = findMimetype(this, src); 22454 } 22455 22456 // update `currentSource` cache always 22457 this.cache_.source = mergeOptions({}, srcObj, { src: src, type: type }); 22458 22459 var matchingSources = this.cache_.sources.filter(function (s) { 22460 return s.src && s.src === src; 22461 }); 22462 var sourceElSources = []; 22463 var sourceEls = this.$$('source'); 22464 var matchingSourceEls = []; 22465 22466 for (var i = 0; i < sourceEls.length; i++) { 22467 var sourceObj = getAttributes(sourceEls[i]); 22468 22469 sourceElSources.push(sourceObj); 22470 22471 if (sourceObj.src && sourceObj.src === src) { 22472 matchingSourceEls.push(sourceObj.src); 22473 } 22474 } 22475 22476 // if we have matching source els but not matching sources 22477 // the current source cache is not up to date 22478 if (matchingSourceEls.length && !matchingSources.length) { 22479 this.cache_.sources = sourceElSources; 22480 // if we don't have matching source or source els set the 22481 // sources cache to the `currentSource` cache 22482 } else if (!matchingSources.length) { 22483 this.cache_.sources = [this.cache_.source]; 22484 } 22485 22486 // update the tech `src` cache 22487 this.cache_.src = src; 22488 }; 22489 22490 /**
22491 * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech} 22492 * causing the media element to reload. 22493 * 22494 * It will fire for the initial source and each subsequent source. 22495 * This event is a custom event from Video.js and is triggered by the {@link Tech}. 22496 * 22497 * The event object for this event contains a `src` property that will contain the source 22498 * that was available when the event was triggered. This is generally only necessary if Video.js 22499 * is switching techs while the source was being changed. 22500 * 22501 * It is also fired when `load` is called on the player (or media element) 22502 * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`} 22503 * says that the resource selection algorithm needs to be aborted and restarted. 22504 * In this case, it is very likely that the `src` property will be set to the 22505 * empty string `""` to indicate we do not know what the source will be but 22506 * that it is changing. 22507 * 22508 * *This event is currently still experimental and may change in minor releases.* 22509 * __To use this, pass `enableSourceset` option to the player.__ 22510 * 22511 * @event Player#sourceset 22512 * @type {EventTarget~Event} 22513 * @prop {string} src 22514 * The source url available when the `sourceset` was triggered. 22515 * It will be an empty string if we cannot know what the source is 22516 * but know that the source will change. 22517 */ 22518 /** 22519 * Retrigger the `sourceset` event that was triggered by the {@link Tech}. 22520 * 22521 * @fires Player#sourceset 22522 * @listens Tech#sourceset 22523 * @private 22524 */ 22525 22526 22527 Player.prototype.handleTechSourceset_ = function handleTechSourceset_(event) { 22528 var _this4 = this; 22529 22530 // only update the source cache when the source 22531 // was not updated using the player api 22532 if (!this.changingSrc_) { 22533 // update the source to the intial source right away 22534 // in some cases this will be empty string 22535 this.updateSourceCaches_(event.src); 22536 22537 // if the `sourceset` `src` was an empty string 22538 // wait for a `loadstart` to update the cache to `currentSrc`. 22539 // If a sourceset happens before a `loadstart`, we reset the state 22540 // as this function will be called again. 22541 if (!event.src) { 22542 var updateCache = function updateCache(e) { 22543 if (e.type !== 'sourceset') { 22544 _this4.updateSourceCaches_(_this4.techGet_('currentSrc')); 22545 } 22546 22547 _this4.tech_.off(['sourceset', 'loadstart'], updateCache); 22548 }; 22549 22550 this.tech_.one(['sourceset', 'loadstart'], updateCache); 22551 } 22552 } 22553 22554 this.trigger({ 22555 src: event.src, 22556 type: 'sourceset' 22557 }); 22558 }; 22559 22560 /** 22561 * Add/remove the vjs-has-started class 22562 * 22563 * @fires Player#firstplay 22564 * 22565 * @param {boolean} request 22566 * - true: adds the class 22567 * - false: remove the class 22568 * 22569 * @return {boolean} 22570 * the boolean value of hasStarted_ 22571 */ 22572 22573 22574 Player.prototype.hasStarted = function hasStarted(request) { 22575 if (request === undefined) { 22576 // act as getter, if we have no request to change 22577 return this.hasStarted_; 22578 } 22579 22580 if (request === this.hasStarted_) { 22581 return; 22582 } 22583 22584 this.hasStarted_ = request; 22585 22586 if (this.hasStarted_) { 22587 this.addClass('vjs-has-started'); 22588 this.trigger('firstplay'); 22589 } else { 22590 this.removeClass('vjs-has-started'); 22591 } 22592 }; 22593 22594 /** 22595 * Fired whenever the media begins or resumes playback 22596 * 22597 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play} 22598 * @fires Player#play 22599 * @listens Tech#play 22600 * @private 22601 */ 22602 22603 22604 Player.prototype.handleTechPlay_ = function handleTechPlay_() { 22605 this.removeClass('vjs-ended'); 22606 this.removeClass('vjs-paused'); 22607 this.addClass('vjs-playing'); 22608 22609 // hide the poster when the user hits play 22610 this.hasStarted(true); 22611 /** 22612 * Triggered whenever an {@link Tech#play} event happens. Indicates that 22613 * playback has started or resumed. 22614 * 22615 * @event Player#play 22616 * @type {EventTarget~Event} 22617 */ 22618 this.trigger('play'); 22619 }; 22620 22621 /** 22622 * Retrigger the `ratechange` event that was triggered by the {@link Tech}. 22623 * 22624 * If there were any events queued while the playback rate was zero, fire 22625 * those events now. 22626 * 22627 * @private 22628 * @method Player#handleTechRateChange_ 22629 * @fires Player#ratechange 22630 * @listens Tech#ratechange 22631 */ 22632 22633 22634 Player.prototype.handleTechRateChange_ = function handleTechRateChange_() { 22635 if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) { 22636 this.queuedCallbacks_.forEach(function (queued) { 22637 return queued.callback(queued.event); 22638 }); 22639 this.queuedCallbacks_ = []; 22640 } 22641 this.cache_.lastPlaybackRate = this.tech_.playbackRate(); 22642 /** 22643 * Fires when the playing speed of the audio/video is changed 22644 * 22645 * @event Player#ratechange 22646 * @type {event} 22647 */ 22648 this.trigger('ratechange'); 22649 }; 22650 22651 /** 22652 * Retrigger the `waiting` event that was triggered by the {@link Tech}. 22653 * 22654 * @fires Player#waiting 22655 * @listens Tech#waiting 22656 * @private 22657 */ 22658 22659 22660 Player.prototype.handleTechWaiting_ = function handleTechWaiting_() { 22661 var _this5 = this; 22662 22663 this.addClass('vjs-waiting'); 22664 /** 22665 * A readyState change on the DOM element has caused playback to stop. 22666 * 22667 * @event Player#waiting 22668 * @type {EventTarget~Event} 22669 */ 22670 this.trigger('waiting'); 22671 this.one('timeupdate', function () { 22672 return _this5.removeClass('vjs-waiting'); 22673 }); 22674 }; 22675 22676 /** 22677 * Retrigger the `canplay` event that was triggered by the {@link Tech}. 22678 * > Note: This is not consistent between browsers. See #1351 22679 * 22680 * @fires Player#canplay 22681 * @listens Tech#canplay 22682 * @private 22683 */ 22684 22685 22686 Player.prototype.handleTechCanPlay_ = function handleTechCanPlay_() { 22687 this.removeClass('vjs-waiting'); 22688 /** 22689 * The media has a readyState of HAVE_FUTURE_DATA or greater. 22690 * 22691 * @event Player#canplay 22692 * @type {EventTarget~Event} 22693 */ 22694 this.trigger('canplay'); 22695 }; 22696 22697 /** 22698 * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}. 22699 * 22700 * @fires Player#canplaythrough 22701 * @listens Tech#canplaythrough 22702 * @private 22703 */ 22704 22705 22706 Player.prototype.handleTechCanPlayThrough_ = function handleTechCanPlayThrough_() { 22707 this.removeClass('vjs-waiting'); 22708 /** 22709 * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the 22710 * entire media file can be played without buffering. 22711 * 22712 * @event Player#canplaythrough 22713 * @type {EventTarget~Event} 22714 */ 22715 this.trigger('canplaythrough'); 22716 }; 22717 22718 /** 22719 * Retrigger the `playing` event that was triggered by the {@link Tech}. 22720 * 22721 * @fires Player#playing 22722 * @listens Tech#playing 22723 * @private 22724 */ 22725 22726 22727 Player.prototype.handleTechPlaying_ = function handleTechPlaying_() { 22728 this.removeClass('vjs-waiting'); 22729 /** 22730 * The media is no longer blocked from playback, and has started playing. 22731 * 22732 * @event Player#playing 22733 * @type {EventTarget~Event} 22734 */ 22735 this.trigger('playing'); 22736 }; 22737 22738 /** 22739 * Retrigger the `seeking` event that was triggered by the {@link Tech}. 22740 * 22741 * @fires Player#seeking 22742 * @listens Tech#seeking 22743 * @private 22744 */ 22745 22746 22747 Player.prototype.handleTechSeeking_ = function handleTechSeeking_() { 22748 this.addClass('vjs-seeking'); 22749 /** 22750 * Fired whenever the player is jumping to a new time 22751 * 22752 * @event Player#seeking 22753 * @type {EventTarget~Event} 22754 */ 22755 this.trigger('seeking'); 22756 }; 22757 22758 /** 22759 * Retrigger the `seeked` event that was triggered by the {@link Tech}. 22760 * 22761 * @fires Player#seeked 22762 * @listens Tech#seeked 22763 * @private 22764 */ 22765 22766 22767 Player.prototype.handleTechSeeked_ = function handleTechSeeked_() { 22768 this.removeClass('vjs-seeking'); 22769 /** 22770 * Fired when the player has finished jumping to a new time 22771 * 22772 * @event Player#seeked 22773 * @type {EventTarget~Event} 22774 */ 22775 this.trigger('seeked'); 22776 }; 22777 22778 /** 22779 * Retrigger the `firstplay` event that was triggered by the {@link Tech}. 22780 * 22781 * @fires Player#firstplay 22782 * @listens Tech#firstplay 22783 * @deprecated As of 6.0 firstplay event is deprecated. 22784 * @deprecated As of 6.0 passing the `starttime` option to the player and the firstplay event are deprecated. 22785 * @private 22786 */ 22787 22788 22789 Player.prototype.handleTechFirstPlay_ = function handleTechFirstPlay_() { 22790 // If the first starttime attribute is specified 22791 // then we will start at the given offset in seconds 22792 if (this.options_.starttime) { 22793 log$1.warn('Passing the `starttime` option to the player will be deprecated
vendor: 18,934 bytes, lines 22793-23493
22793in 6.0'); 22794 this.currentTime(this.options_.starttime); 22795 } 22796 22797 this.addClass('vjs-has-started'); 22798 /** 22799 * Fired the first time a video is played. Not part of the HLS spec, and this is 22800 * probably not the best implementation yet, so use sparingly. If you don't have a 22801 * reason to prevent playback, use `myPlayer.one('play');` instead. 22802 * 22803 * @event Player#firstplay 22804 * @deprecated As of 6.0 firstplay event is deprecated. 22805 * @type {EventTarget~Event} 22806 */ 22807 this.trigger('firstplay'); 22808 }; 22809 22810 /** 22811 * Retrigger the `pause` event that was triggered by the {@link Tech}. 22812 * 22813 * @fires Player#pause 22814 * @listens Tech#pause 22815 * @private 22816 */ 22817 22818 22819 Player.prototype.handleTechPause_ = function handleTechPause_() { 22820 this.removeClass('vjs-playing'); 22821 this.addClass('vjs-paused'); 22822 /** 22823 * Fired whenever the media has been paused 22824 * 22825 * @event Player#pause 22826 * @type {EventTarget~Event} 22827 */ 22828 this.trigger('pause'); 22829 }; 22830 22831 /** 22832 * Retrigger the `ended` event that was triggered by the {@link Tech}. 22833 * 22834 * @fires Player#ended 22835 * @listens Tech#ended 22836 * @private 22837 */ 22838 22839 22840 Player.prototype.handleTechEnded_ = function handleTechEnded_() { 22841 this.addClass('vjs-ended'); 22842 if (this.options_.loop) { 22843 this.currentTime(0); 22844 this.play(); 22845 } else if (!this.paused()) { 22846 this.pause(); 22847 } 22848 22849 /** 22850 * Fired when the end of the media resource is reached (currentTime == duration) 22851 * 22852 * @event Player#ended 22853 * @type {EventTarget~Event} 22854 */ 22855 this.trigger('ended'); 22856 }; 22857 22858 /** 22859 * Fired when the duration of the media resource is first known or changed 22860 * 22861 * @listens Tech#durationchange 22862 * @private 22863 */ 22864 22865 22866 Player.prototype.handleTechDurationChange_ = function handleTechDurationChange_() { 22867 this.duration(this.techGet_('duration')); 22868 }; 22869 22870 /** 22871 * Handle a click on the media element to play/pause 22872 * 22873 * @param {EventTarget~Event} event 22874 * the event that caused this function to trigger 22875 * 22876 * @listens Tech#mousedown 22877 * @private 22878 */ 22879 22880 22881 Player.prototype.handleTechClick_ = function handleTechClick_(event) { 22882 if (!isSingleLeftClick(event)) { 22883 return; 22884 } 22885 22886 // When controls are disabled a click should not toggle playback because 22887 // the click is considered a control 22888 if (!this.controls_) { 22889 return; 22890 } 22891 22892 if (this.paused()) { 22893 silencePromise(this.play()); 22894 } else { 22895 this.pause(); 22896 } 22897 }; 22898 22899 /** 22900 * Handle a tap on the media element. It will toggle the user 22901 * activity state, which hides and shows the controls. 22902 * 22903 * @listens Tech#tap 22904 * @private 22905 */ 22906 22907 22908 Player.prototype.handleTechTap_ = function handleTechTap_() { 22909 this.userActive(!this.userActive()); 22910 }; 22911 22912 /** 22913 * Handle touch to start 22914 * 22915 * @listens Tech#touchstart 22916 * @private 22917 */ 22918 22919 22920 Player.prototype.handleTechTouchStart_ = function handleTechTouchStart_() { 22921 this.userWasActive = this.userActive(); 22922 }; 22923 22924 /** 22925 * Handle touch to move 22926 * 22927 * @listens Tech#touchmove 22928 * @private 22929 */ 22930 22931 22932 Player.prototype.handleTechTouchMove_ = function handleTechTouchMove_() { 22933 if (this.userWasActive) { 22934 this.reportUserActivity(); 22935 } 22936 }; 22937 22938 /** 22939 * Handle touch to end 22940 * 22941 * @param {EventTarget~Event} event 22942 * the touchend event that triggered 22943 * this function 22944 * 22945 * @listens Tech#touchend 22946 * @private 22947 */ 22948 22949 22950 Player.prototype.handleTechTouchEnd_ = function handleTechTouchEnd_(event) { 22951 // Stop the mouse events from also happening 22952 event.preventDefault(); 22953 }; 22954 22955 /** 22956 * Fired when the player switches in or out of fullscreen mode 22957 * 22958 * @private 22959 * @listens Player#fullscreenchange 22960 */ 22961 22962 22963 Player.prototype.handleFullscreenChange_ = function handleFullscreenChange_() { 22964 if (this.isFullscreen()) { 22965 this.addClass('vjs-fullscreen'); 22966 } else { 22967 this.removeClass('vjs-fullscreen'); 22968 } 22969 }; 22970 22971 /** 22972 * native click events on the SWF aren't triggered on IE11, Win8.1RT 22973 * use stageclick events triggered from inside the SWF instead 22974 * 22975 * @private 22976 * @listens stageclick 22977 */ 22978 22979 22980 Player.prototype.handleStageClick_ = function handleStageClick_() { 22981 this.reportUserActivity(); 22982 }; 22983 22984 /** 22985 * Handle Tech Fullscreen Change 22986 * 22987 * @param {EventTarget~Event} event 22988 * the fullscreenchange event that triggered this function 22989 * 22990 * @param {Object} data 22991 * the data that was sent with the event 22992 * 22993 * @private 22994 * @listens Tech#fullscreenchange 22995 * @fires Player#fullscreenchange 22996 */ 22997 22998 22999 Player.prototype.handleTechFullscreenChange_ = function handleTechFullscreenChange_(event, data) { 23000 if (data) { 23001 this.isFullscreen(data.isFullscreen); 23002 } 23003 /** 23004 * Fired when going in and out of fullscreen. 23005 * 23006 * @event Player#fullscreenchange 23007 * @type {EventTarget~Event} 23008 */ 23009 this.trigger('fullscreenchange'); 23010 }; 23011 23012 /** 23013 * Fires when an error occurred during the loading of an audio/video. 23014 * 23015 * @private 23016 * @listens Tech#error 23017 */ 23018 23019 23020 Player.prototype.handleTechError_ = function handleTechError_() { 23021 var error = this.tech_.error(); 23022 23023 this.error(error); 23024 }; 23025 23026 /** 23027 * Retrigger the `textdata` event that was triggered by the {@link Tech}. 23028 * 23029 * @fires Player#textdata 23030 * @listens Tech#textdata 23031 * @private 23032 */ 23033 23034 23035 Player.prototype.handleTechTextData_ = function handleTechTextData_() { 23036 var data = null; 23037 23038 if (arguments.length > 1) { 23039 data = arguments[1]; 23040 } 23041 23042 /** 23043 * Fires when we get a textdata event from tech 23044 * 23045 * @event Player#textdata 23046 * @type {EventTarget~Event} 23047 */ 23048 this.trigger('textdata', data); 23049 }; 23050 23051 /** 23052 * Get object for cached values. 23053 * 23054 * @return {Object} 23055 * get the current object cache 23056 */ 23057 23058 23059 Player.prototype.getCache = function getCache() { 23060 return this.cache_; 23061 }; 23062 23063 /** 23064 * Pass values to the playback tech 23065 * 23066 * @param {string} [method] 23067 * the method to call 23068 * 23069 * @param {Object} arg 23070 * the argument to pass 23071 * 23072 * @private 23073 */ 23074 23075 23076 Player.prototype.techCall_ = function techCall_(method, arg) { 23077 // If it's not ready yet, call method when it is 23078 23079 this.ready(function () { 23080 if (method in allowedSetters) { 23081 return set$1(this.middleware_, this.tech_, method, arg); 23082 } else if (method in allowedMediators) { 23083 return mediate(this.middleware_, this.tech_, method, arg); 23084 } 23085 23086 try { 23087 if (this.tech_) { 23088 this.tech_[method](arg); 23089 } 23090 } catch (e) { 23091 log$1(e); 23092 throw e; 23093 } 23094 }, true); 23095 }; 23096 23097 /** 23098 * Get calls can't wait for the tech, and sometimes don't need to. 23099 * 23100 * @param {string} method 23101 * Tech method 23102 * 23103 * @return {Function|undefined} 23104 * the method or undefined 23105 * 23106 * @private 23107 */ 23108 23109 23110 Player.prototype.techGet_ = function techGet_(method) { 23111 if (!this.tech_ || !this.tech_.isReady_) { 23112 return; 23113 } 23114 23115 if (method in allowedGetters) { 23116 return get$1(this.middleware_, this.tech_, method); 23117 } else if (method in allowedMediators) { 23118 return mediate(this.middleware_, this.tech_, method); 23119 } 23120 23121 // Flash likes to die and reload when you hide or reposition it. 23122 // In these cases the object methods go away and we get errors. 23123 // When that happens we'll catch the errors and inform tech that it's not ready any more. 23124 try { 23125 return this.tech_[method](); 23126 } catch (e) { 23127 23128 // When building additional tech libs, an expected method may not be defined yet 23129 if (this.tech_[method] === undefined) { 23130 log$1('Video.js: ' + method + ' method not defined for ' + this.techName_ + ' playback technology.', e); 23131 throw e; 23132 } 23133 23134 // When a method isn't available on the object it throws a TypeError 23135 if (e.name === 'TypeError') { 23136 log$1('Video.js: ' + method + ' unavailable on ' + this.techName_ + ' playback technology element.', e); 23137 this.tech_.isReady_ = false; 23138 throw e; 23139 } 23140 23141 // If error unknown, just log and throw 23142 log$1(e); 23143 throw e; 23144 } 23145 }; 23146 23147 /** 23148 * Attempt to begin playback at the first opportunity. 23149 * 23150 * @return {Promise|undefined} 23151 * Returns a `Promise` only if the browser returns one and the player 23152 * is ready to begin playback. For some browsers and all non-ready 23153 * situations, this will return `undefined`. 23154 */ 23155 23156 23157 Player.prototype.play = function play() { 23158 var _this6 = this; 23159 23160 // If this is called while we have a play queued up on a loadstart, remove 23161 // that listener to avoid getting in a potentially bad state. 23162 if (this.playOnLoadstart_) { 23163 this.off('loadstart', this.playOnLoadstart_); 23164 } 23165 23166 // If the player/tech is not ready, queue up another call to `play()` for 23167 // when it is. This will loop back into this method for another attempt at 23168 // playback when the tech is ready. 23169 if (!this.isReady_) { 23170 23171 // Bail out if we're already waiting for `ready`! 23172 if (this.playWaitingForReady_) { 23173 return; 23174 } 23175 23176 this.playWaitingForReady_ = true; 23177 this.ready(function () { 23178 _this6.playWaitingForReady_ = false; 23179 silencePromise(_this6.play()); 23180 }); 23181 23182 // If the player/tech is ready and we have a source, we can attempt playback. 23183 } else if (!this.changingSrc_ && (this.src() || this.currentSrc())) { 23184 return this.techGet_('play'); 23185 23186 // If the tech is ready, but we do not have a source, we'll need to wait 23187 // for both the `ready` and a `loadstart` when the source is finally 23188 // resolved by middleware and set on the player. 23189 // 23190 // This can happen if `play()` is called while changing sources or before 23191 // one has been set on the player. 23192 } else { 23193 23194 this.playOnLoadstart_ = function () { 23195 _this6.playOnLoadstart_ = null; 23196 silencePromise(_this6.play()); 23197 }; 23198 23199 this.one('loadstart', this.playOnLoadstart_); 23200 } 23201 }; 23202 23203 /** 23204 * Pause the video playback 23205 * 23206 * @return {Player} 23207 * A reference to the player object this function was called on 23208 */ 23209 23210 23211 Player.prototype.pause = function pause() { 23212 this.techCall_('pause'); 23213 }; 23214 23215 /** 23216 * Check if the player is paused or has yet to play 23217 * 23218 * @return {boolean} 23219 * - false: if the media is currently playing 23220 * - true: if media is not currently playing 23221 */ 23222 23223 23224 Player.prototype.paused = function paused() { 23225 // The initial state of paused should be true (in Safari it's actually false) 23226 return this.techGet_('paused') === false ? false : true; 23227 }; 23228 23229 /** 23230 * Get a TimeRange object representing the current ranges of time that the user 23231 * has played. 23232 * 23233 * @return {TimeRange} 23234 * A time range object that represents all the increments of time that have 23235 * been played. 23236 */ 23237 23238 23239 Player.prototype.played = function played() { 23240 return this.techGet_('played') || createTimeRanges(0, 0); 23241 }; 23242 23243 /** 23244 * Returns whether or not the user is "scrubbing". Scrubbing is 23245 * when the user has clicked the progress bar handle and is 23246 * dragging it along the progress bar. 23247 * 23248 * @param {boolean} [isScrubbing] 23249 * whether the user is or is not scrubbing 23250 * 23251 * @return {boolean} 23252 * The value of scrubbing when getting 23253 */ 23254 23255 23256 Player.prototype.scrubbing = function scrubbing(isScrubbing) { 23257 if (typeof isScrubbing === 'undefined') { 23258 return this.scrubbing_; 23259 } 23260 this.scrubbing_ = !!isScrubbing; 23261 23262 if (isScrubbing) { 23263 this.addClass('vjs-scrubbing'); 23264 } else { 23265 this.removeClass('vjs-scrubbing'); 23266 } 23267 }; 23268 23269 /** 23270 * Get or set the current time (in seconds) 23271 * 23272 * @param {number|string} [seconds] 23273 * The time to seek to in seconds 23274 * 23275 * @return {number} 23276 * - the current time in seconds when getting 23277 */ 23278 23279 23280 Player.prototype.currentTime = function currentTime(seconds) { 23281 if (typeof seconds !== 'undefined') { 23282 if (seconds < 0) { 23283 seconds = 0; 23284 } 23285 this.techCall_('setCurrentTime', seconds); 23286 return; 23287 } 23288 23289 // cache last currentTime and return. default to 0 seconds 23290 // 23291 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's 23292 // currentTime when scrubbing, but may not provide much performance benefit afterall. 23293 // Should be tested. Also something has to read the actual current time or the cache will 23294 // never get updated. 23295 this.cache_.currentTime = this.techGet_('currentTime') || 0; 23296 return this.cache_.currentTime; 23297 }; 23298 23299 /** 23300 * Normally gets the length in time of the video in seconds; 23301 * in all but the rarest use cases an argument will NOT be passed to the method 23302 * 23303 * > **NOTE**: The video must have started loading before the duration can be 23304 * known, and in the case of Flash, may not be known until the video starts 23305 * playing. 23306 * 23307 * @fires Player#durationchange 23308 * 23309 * @param {number} [seconds] 23310 * The duration of the video to set in seconds 23311 * 23312 * @return {number} 23313 * - The duration of the video in seconds when getting 23314 */ 23315 23316 23317 Player.prototype.duration = function duration(seconds) { 23318 if (seconds === undefined) { 23319 // return NaN if the duration is not known 23320 return this.cache_.duration !== undefined ? this.cache_.duration : NaN; 23321 } 23322 23323 seconds = parseFloat(seconds); 23324 23325 // Standardize on Infinity for signaling video is live 23326 if (seconds < 0) { 23327 seconds = Infinity; 23328 } 23329 23330 if (seconds !== this.cache_.duration) { 23331 // Cache the last set value for optimized scrubbing (esp. Flash) 23332 this.cache_.duration = seconds; 23333 23334 if (seconds === Infinity) { 23335 this.addClass('vjs-live'); 23336 } else { 23337 this.removeClass('vjs-live'); 23338 } 23339 /** 23340 * @event Player#durationchange 23341 * @type {EventTarget~Event} 23342 */ 23343 this.trigger('durationchange'); 23344 } 23345 }; 23346 23347 /** 23348 * Calculates how much time is left in the video. Not part 23349 * of the native video API. 23350 * 23351 * @return {number} 23352 * The time remaining in seconds 23353 */ 23354 23355 23356 Player.prototype.remainingTime = function remainingTime() { 23357 return this.duration() - this.currentTime(); 23358 }; 23359 23360 /** 23361 * A remaining time function that is intented to be used when 23362 * the time is to be displayed directly to the user. 23363 * 23364 * @return {number} 23365 * The rounded time remaining in seconds 23366 */ 23367 23368 23369 Player.prototype.remainingTimeDisplay = function remainingTimeDisplay() { 23370 return Math.floor(this.duration()) - Math.floor(this.currentTime()); 23371 }; 23372 23373 // 23374 // Kind of like an array of portions of the video that have been downloaded. 23375 23376 /** 23377 * Get a TimeRange object with an array of the times of the video 23378 * that have been downloaded. If you just want the percent of the 23379 * video that's been downloaded, use bufferedPercent. 23380 * 23381 * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered} 23382 * 23383 * @return {TimeRange} 23384 * A mock TimeRange object (following HTML spec) 23385 */ 23386 23387 23388 Player.prototype.buffered = function buffered() { 23389 var buffered = this.techGet_('buffered'); 23390 23391 if (!buffered || !buffered.length) { 23392 buffered = createTimeRanges(0, 0); 23393 } 23394 23395 return buffered; 23396 }; 23397 23398 /** 23399 * Get the percent (as a decimal) of the video that's been downloaded. 23400 * This method is not a part of the native HTML video API. 23401 * 23402 * @return {number} 23403 * A decimal between 0 and 1 representing the percent 23404 * that is buffered 0 being 0% and 1 being 100% 23405 */ 23406 23407 23408 Player.prototype.bufferedPercent = function bufferedPercent$$1() { 23409 return bufferedPercent(this.buffered(), this.duration()); 23410 }; 23411 23412 /** 23413 * Get the ending time of the last buffered time range 23414 * This is used in the progress bar to encapsulate all time ranges. 23415 * 23416 * @return {number} 23417 * The end of the last buffered time range 23418 */ 23419 23420 23421 Player.prototype.bufferedEnd = function bufferedEnd() { 23422 var buffered = this.buffered(); 23423 var duration = this.duration(); 23424 var end = buffered.end(buffered.length - 1); 23425 23426 if (end > duration) { 23427 end = duration; 23428 } 23429 23430 return end; 23431 }; 23432 23433 /** 23434 * Get or set the current volume of the media 23435 * 23436 * @param {number} [percentAsDecimal] 23437 * The new volume as a decimal percent: 23438 * - 0 is muted/0%/off 23439 * - 1.0 is 100%/full 23440 * - 0.5 is half volume or 50% 23441 * 23442 * @return {number} 23443 * The current volume as a percent when getting 23444 */ 23445 23446 23447 Player.prototype.volume = function volume(percentAsDecimal) { 23448 var vol = void 0; 23449 23450 if (percentAsDecimal !== undefined) { 23451 // Force value to between 0 and 1 23452 vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); 23453 this.cache_.volume = vol; 23454 this.techCall_('setVolume', vol); 23455 23456 if (vol > 0) { 23457 this.lastVolume_(vol); 23458 } 23459 23460 return; 23461 } 23462 23463 // Default to 1 when returning current volume. 23464 vol = parseFloat(this.techGet_('volume')); 23465 return isNaN(vol) ? 1 : vol; 23466 }; 23467 23468 /** 23469 * Get the current muted state, or turn mute on or off 23470 * 23471 * @param {boolean} [muted] 23472 * - true to mute 23473 * - false to unmute 23474 * 23475 * @return {boolean} 23476 * - true if mute is on and getting 23477 * - false if mute is off and getting 23478 */ 23479 23480 23481 Player.prototype.muted = function muted(_muted) { 23482 if (_muted !== undefined) { 23483 this.techCall_('setMuted', _muted); 23484 return; 23485 } 23486 return this.techGet_('muted') || false; 23487 }; 23488 23489 /** 23490 * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted 23491 * indicates the state of muted on initial playback. 23492 * 23493 * ```js
23494 * var myPlayer = videojs('some-player-id'); 23495 * 23496 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 23497 * 23498 * // get, should be false 23499 * console.log(myPlayer.defaultMuted()); 23500 * // set to true 23501 * myPlayer.defaultMuted(true); 23502 * // get should be true 23503 * console.log(myPlayer.defaultMuted()); 23504 * ``` 23505 * 23506 * @param {boolean} [defaultMuted] 23507 * - true to mute 23508 * - false to unmute 23509 * 23510 * @return {boolean|Player} 23511 * - true if defaultMuted is on and getting 23512 * - false if defaultMuted is off and getting 23513 * - A reference to the current player when setting 23514 */ 23515 23516 23517 Player.prototype.defaultMuted = function defaultMuted(_defaultMuted) { 23518 if (_defaultMuted !== undefined) { 23519 return this.techCall_('setDefaultMuted', _defaultMuted); 23520 } 23521 return this.techGet_('defaultMuted') || false; 23522 }; 23523 23524 /** 23525 * Get the last volume, or set it 23526 * 23527 * @param {number} [percentAsDecimal] 23528 * The new last volume as a decimal percent: 23529 * - 0 is muted/0%/off 23530 * - 1.0 is 100%/full 23531 * - 0.5 is half volume or 50% 23532 * 23533 * @return {number} 23534 * the current value of lastVolume as a percent when getting 23535 * 23536 * @private 23537 */ 23538 23539 23540 Player.prototype.lastVolume_ = function lastVolume_(percentAsDecimal) { 23541 if (percentAsDecimal !== undefined && percentAsDecimal !== 0) { 23542 this.cache_.lastVolume = percentAsDecimal; 23543 return; 23544 } 23545 return this.cache_.lastVolume; 23546 }; 23547 23548 /** 23549 * Check if current tech can support native fullscreen 23550 * (e.g. with built in controls like iOS, so not our flash swf) 23551 * 23552 * @return {boolean} 23553 * if native fullscreen is supported 23554 */ 23555 23556 23557 Player.prototype.supportsFullScreen = function supportsFullScreen() { 23558 return this.techGet_('supportsFullScreen') || false; 23559 }; 23560 23561 /** 23562 * Check if the player is in fullscreen mode or tell the player that it 23563 * is or is not in fullscreen mode. 23564 * 23565 * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 23566 * property and instead document.fullscreenElement is used. But isFullscreen is 23567 * still a valuable property for internal player workings. 23568 * 23569 * @param {boolean} [isFS] 23570 * Set the players current fullscreen state 23571 * 23572 * @return {boolean} 23573 * - true if fullscreen is on and getting 23574 * - false if fullscreen is off and getting 23575 */ 23576 23577 23578 Player.prototype.isFullscreen = function isFullscreen(isFS) { 23579 if (isFS !== undefined) { 23580 this.isFullscreen_ = !!isFS; 23581 return; 23582 } 23583 return !!this.isFullscreen_; 23584 }; 23585 23586 /** 23587 * Increase the size of the video to full screen 23588 * In some browsers, full screen is not supported natively, so it enters 23589 * "full window mode", where the video fills the browser window. 23590 * In browsers and devices that support native full screen, sometimes the 23591 * browser's default controls will be shown, and not the Video.js custom skin. 23592 * This includes most mobile devices (iOS, Android) and older versions of 23593 * Safari. 23594 * 23595 * @fires Player#fullscreenchange 23596 */ 23597 23598 23599 Player.prototype.requestFullscreen = function requestFullscreen() { 23600 var fsApi = FullscreenApi; 23601 23602 this.isFullscreen(true); 23603 23604 if (fsApi.requestFullscreen) { 23605 // the browser supports going fullscreen at the element level so we can 23606 // take the controls fullscreen as well as the video 23607 23608 // Trigger fullscreenchange event after change 23609 // We have to specifically add this each time, and remove 23610 // when canceling fullscreen. Otherwise if there's multiple 23611 // players on a page, they would all be reacting to the same fullscreen 23612 // events 23613 on(document_1, fsApi.fullscreenchange, bind(this, function documentFullscreenChange(e) { 23614 this.isFullscreen(document_1[fsApi.fullscreenElement]); 23615 23616 // If cancelling fullscreen, remove event listener. 23617 if (this.isFullscreen() === false) { 23618 off(document_1, fsApi.fullscreenchange, documentFullscreenChange); 23619 } 23620 /** 23621 * @event Player#fullscreenchange 23622 * @type {EventTarget~Event} 23623 */ 23624 this.trigger('fullscreenchange'); 23625 })); 23626 23627 this.el_[fsApi.requestFullscreen](); 23628 } else if (this.tech_.supportsFullScreen()) { 23629 // we can't take the video.js controls fullscreen but we can go fullscreen 23630 // with native controls 23631 this.techCall_('enterFullScreen'); 23632 } else { 23633 // fullscreen isn't supported so we'll just stretch the video element to 23634 // fill the viewport 23635 this.enterFullWindow(); 23636 /** 23637 * @event Player#fullscreenchange 23638 * @type {EventTarget~Event} 23639 */ 23640 this.trigger('fullscreenchange'); 23641 } 23642 }; 23643 23644 /** 23645 * Return the video to its normal size after having been in full screen mode 23646 * 23647 * @fires Player#fullscreenchange 23648 */ 23649 23650 23651 Player.prototype.exitFullscreen = function exitFullscreen() { 23652 var fsApi = FullscreenApi; 23653 23654 this.isFullscreen(false); 23655 23656 // Check for browser element fullscreen support 23657 if (fsApi.requestFullscreen) { 23658 document_1[fsApi.exitFullscreen](); 23659 } else if (this.tech_.supportsFullScreen()) { 23660 this.techCall_('exitFullScreen'); 23661 } else { 23662 this.exitFullWindow(); 23663 /** 23664 * @event Player#fullscreenchange 23665 * @type {EventTarget~Event} 23666 */ 23667 this.trigger('fullscreenchange'); 23668 } 23669 }; 23670 23671 /** 23672 * When fullscreen isn't supported we can stretch the 23673 * video container to as wide as the browser will let us. 23674 * 23675 * @fires Player#enterFullWindow 23676 */ 23677 23678 23679 Player.prototype.enterFullWindow = function enterFullWindow() { 23680 this.isFullWindow = true; 23681 23682 // Storing original doc overflow value to return to when fullscreen is off 23683 this.docOrigOverflow = document_1.documentElement.style.overflow; 23684 23685 // Add listener for esc key to exit fullscreen 23686 on(document_1, 'keydown', bind(this, this.fullWindowOnEscKey)); 23687 23688 // Hide any scroll bars 23689 document_1.documentElement.style.overflow = 'hidden'; 23690 23691 // Apply fullscreen styles 23692 addClass(document_1.body, 'vjs-full-window'); 23693 23694 /** 23695 * @event Player#enterFullWindow 23696 * @type {EventTarget~Event} 23697 */ 23698 this.trigger('enterFullWindow'); 23699 }; 23700 23701 /** 23702 * Check for call to either exit full window or 23703 * full screen on ESC key 23704 * 23705 * @param {string} event 23706 * Event to check for key press 23707 */ 23708 23709 23710 Player.prototype.fullWindowOnEscKey = function fullWindowOnEscKey(event) { 23711 if (event.keyCode === 27) { 23712 if (this.isFullscreen() === true) { 23713 this.exitFullscreen(); 23714 } else { 23715 this.exitFullWindow(); 23716 } 23717 } 23718 }; 23719 23720 /** 23721 * Exit full window 23722 * 23723 * @fires Player#exitFullWindow 23724 */ 23725 23726 23727 Player.prototype.exitFullWindow = function exitFullWindow() { 23728 this.isFullWindow = false;
vendor: 73,136 bytes, lines 23729-26243
23729 off(document_1, 'keydown', this.fullWindowOnEscKey); 23730 23731 // Unhide scroll bars. 23732 document_1.documentElement.style.overflow = this.docOrigOverflow; 23733 23734 // Remove fullscreen styles 23735 removeClass(document_1.body, 'vjs-full-window'); 23736 23737 // Resize the box, controller, and poster to original sizes 23738 // this.positionAll(); 23739 /** 23740 * @event Player#exitFullWindow 23741 * @type {EventTarget~Event} 23742 */ 23743 this.trigger('exitFullWindow'); 23744 }; 23745 23746 /** 23747 * Check whether the player can play a given mimetype 23748 * 23749 * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype 23750 * 23751 * @param {string} type 23752 * The mimetype to check 23753 * 23754 * @return {string} 23755 * 'probably', 'maybe', or '' (empty string) 23756 */ 23757 23758 23759 Player.prototype.canPlayType = function canPlayType(type) { 23760 var can = void 0; 23761 23762 // Loop through each playback technology in the options order 23763 for (var i = 0, j = this.options_.techOrder; i < j.length; i++) { 23764 var techName = j[i]; 23765 var tech = Tech.getTech(techName); 23766 23767 // Support old behavior of techs being registered as components. 23768 // Remove once that deprecated behavior is removed. 23769 if (!tech) { 23770 tech = Component.getComponent(techName); 23771 } 23772 23773 // Check if the current tech is defined before continuing 23774 if (!tech) { 23775 log$1.error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); 23776 continue; 23777 } 23778 23779 // Check if the browser supports this technology 23780 if (tech.isSupported()) { 23781 can = tech.canPlayType(type); 23782 23783 if (can) { 23784 return can; 23785 } 23786 } 23787 } 23788 23789 return ''; 23790 }; 23791 23792 /** 23793 * Select source based on tech-order or source-order 23794 * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, 23795 * defaults to tech-order selection 23796 * 23797 * @param {Array} sources 23798 * The sources for a media asset 23799 * 23800 * @return {Object|boolean} 23801 * Object of source and tech order or false 23802 */ 23803 23804 23805 Player.prototype.selectSource = function selectSource(sources) { 23806 var _this7 = this; 23807 23808 // Get only the techs specified in `techOrder` that exist and are supported by the 23809 // current platform 23810 var techs = this.options_.techOrder.map(function (techName) { 23811 return [techName, Tech.getTech(techName)]; 23812 }).filter(function (_ref) { 23813 var techName = _ref[0], 23814 tech = _ref[1]; 23815 23816 // Check if the current tech is defined before continuing 23817 if (tech) { 23818 // Check if the browser supports this technology 23819 return tech.isSupported(); 23820 } 23821 23822 log$1.error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); 23823 return false; 23824 }); 23825 23826 // Iterate over each `innerArray` element once per `outerArray` element and execute 23827 // `tester` with both. If `tester` returns a non-falsy value, exit early and return 23828 // that value. 23829 var findFirstPassingTechSourcePair = function findFirstPassingTechSourcePair(outerArray, innerArray, tester) { 23830 var found = void 0; 23831 23832 outerArray.some(function (outerChoice) { 23833 return innerArray.some(function (innerChoice) { 23834 found = tester(outerChoice, innerChoice); 23835 23836 if (found) { 23837 return true; 23838 } 23839 }); 23840 }); 23841 23842 return found; 23843 }; 23844 23845 var foundSourceAndTech = void 0; 23846 var flip = function flip(fn) { 23847 return function (a, b) { 23848 return fn(b, a); 23849 }; 23850 }; 23851 var finder = function finder(_ref2, source) { 23852 var techName = _ref2[0], 23853 tech = _ref2[1]; 23854 23855 if (tech.canPlaySource(source, _this7.options_[techName.toLowerCase()])) { 23856 return { source: source, tech: techName }; 23857 } 23858 }; 23859 23860 // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources 23861 // to select from them based on their priority. 23862 if (this.options_.sourceOrder) { 23863 // Source-first ordering 23864 foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); 23865 } else { 23866 // Tech-first ordering 23867 foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); 23868 } 23869 23870 return foundSourceAndTech || false; 23871 }; 23872 23873 /** 23874 * Get or set the video source. 23875 * 23876 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 23877 * A SourceObject, an array of SourceObjects, or a string referencing 23878 * a URL to a media source. It is _highly recommended_ that an object 23879 * or array of objects is used here, so that source selection 23880 * algorithms can take the `type` into account. 23881 * 23882 * If not provided, this method acts as a getter. 23883 * 23884 * @return {string|undefined} 23885 * If the `source` argument is missing, returns the current source 23886 * URL. Otherwise, returns nothing/undefined. 23887 */ 23888 23889 23890 Player.prototype.src = function src(source) { 23891 var _this8 = this; 23892 23893 // getter usage 23894 if (typeof source === 'undefined') { 23895 return this.cache_.src || ''; 23896 } 23897 // filter out invalid sources and turn our source into 23898 // an array of source objects 23899 var sources = filterSource(source); 23900 23901 // if a source was passed in then it is invalid because 23902 // it was filtered to a zero length Array. So we have to 23903 // show an error 23904 if (!sources.length) { 23905 this.setTimeout(function () { 23906 this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); 23907 }, 0); 23908 return; 23909 } 23910 23911 // intial sources 23912 this.changingSrc_ = true; 23913 23914 this.cache_.sources = sources; 23915 this.updateSourceCaches_(sources[0]); 23916 23917 // middlewareSource is the source after it has been changed by middleware 23918 setSource(this, sources[0], function (middlewareSource, mws) { 23919 _this8.middleware_ = mws; 23920 23921 // since sourceSet is async we have to update the cache again after we select a source since 23922 // the source that is selected could be out of order from the cache update above this callback. 23923 _this8.cache_.sources = sources; 23924 _this8.updateSourceCaches_(middlewareSource); 23925 23926 var err = _this8.src_(middlewareSource); 23927 23928 if (err) { 23929 if (sources.length > 1) { 23930 return _this8.src(sources.slice(1)); 23931 } 23932 23933 _this8.changingSrc_ = false; 23934 23935 // We need to wrap this in a timeout to give folks a chance to add error event handlers 23936 _this8.setTimeout(function () { 23937 this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); 23938 }, 0); 23939 23940 // we could not find an appropriate tech, but let's still notify the delegate that this is it 23941 // this needs a better comment about why this is needed 23942 _this8.triggerReady(); 23943 23944 return; 23945 } 23946 23947 setTech(mws, _this8.tech_); 23948 }); 23949 }; 23950 23951 /** 23952 * Set the source object on the tech, returns a boolean that indicates whether 23953 * there is a tech that can play the source or not 23954 * 23955 * @param {Tech~SourceObject} source 23956 * The source object to set on the Tech 23957 * 23958 * @return {Boolean} 23959 * - True if there is no Tech to playback this source 23960 * - False otherwise 23961 * 23962 * @private 23963 */ 23964 23965 23966 Player.prototype.src_ = function src_(source) { 23967 var _this9 = this; 23968 23969 var sourceTech = this.selectSource([source]); 23970 23971 if (!sourceTech) { 23972 return true; 23973 } 23974 23975 if (!titleCaseEquals(sourceTech.tech, this.techName_)) { 23976 this.changingSrc_ = true; 23977 // load this technology with the chosen source 23978 this.loadTech_(sourceTech.tech, sourceTech.source); 23979 this.tech_.ready(function () { 23980 _this9.changingSrc_ = false; 23981 }); 23982 return false; 23983 } 23984 23985 // wait until the tech is ready to set the source 23986 // and set it synchronously if possible (#2326) 23987 this.ready(function () { 23988 23989 // The setSource tech method was added with source handlers 23990 // so older techs won't support it 23991 // We need to check the direct prototype for the case where subclasses 23992 // of the tech do not support source handlers 23993 if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { 23994 this.techCall_('setSource', source); 23995 } else { 23996 this.techCall_('src', source.src); 23997 } 23998 23999 this.changingSrc_ = false; 24000 }, true); 24001 24002 return false; 24003 }; 24004 24005 /** 24006 * Begin loading the src data. 24007 */ 24008 24009 24010 Player.prototype.load = function load() { 24011 this.techCall_('load'); 24012 }; 24013 24014 /** 24015 * Reset the player. Loads the first tech in the techOrder, 24016 * and calls `reset` on the tech`. 24017 */ 24018 24019 24020 Player.prototype.reset = function reset() { 24021 this.loadTech_(this.options_.techOrder[0], null); 24022 this.techCall_('reset'); 24023 }; 24024 24025 /** 24026 * Returns all of the current source objects. 24027 * 24028 * @return {Tech~SourceObject[]} 24029 * The current source objects 24030 */ 24031 24032 24033 Player.prototype.currentSources = function currentSources() { 24034 var source = this.currentSource(); 24035 var sources = []; 24036 24037 // assume `{}` or `{ src }` 24038 if (Object.keys(source).length !== 0) { 24039 sources.push(source); 24040 } 24041 24042 return this.cache_.sources || sources; 24043 }; 24044 24045 /** 24046 * Returns the current source object. 24047 * 24048 * @return {Tech~SourceObject} 24049 * The current source object 24050 */ 24051 24052 24053 Player.prototype.currentSource = function currentSource() { 24054 return this.cache_.source || {}; 24055 }; 24056 24057 /** 24058 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 24059 * Can be used in conjunction with `currentType` to assist in rebuilding the current source object. 24060 * 24061 * @return {string} 24062 * The current source 24063 */ 24064 24065 24066 Player.prototype.currentSrc = function currentSrc() { 24067 return this.currentSource() && this.currentSource().src || ''; 24068 }; 24069 24070 /** 24071 * Get the current source type e.g. video/mp4 24072 * This can allow you rebuild the current source object so that you could load the same 24073 * source and tech later 24074 * 24075 * @return {string} 24076 * The source MIME type 24077 */ 24078 24079 24080 Player.prototype.currentType = function currentType() { 24081 return this.currentSource() && this.currentSource().type || ''; 24082 }; 24083 24084 /** 24085 * Get or set the preload attribute 24086 * 24087 * @param {boolean} [value] 24088 * - true means that we should preload 24089 * - false means that we should not preload 24090 * 24091 * @return {string} 24092 * The preload attribute value when getting 24093 */ 24094 24095 24096 Player.prototype.preload = function preload(value) { 24097 if (value !== undefined) { 24098 this.techCall_('setPreload', value); 24099 this.options_.preload = value; 24100 return; 24101 } 24102 return this.techGet_('preload'); 24103 }; 24104 24105 /** 24106 * Get or set the autoplay attribute. 24107 * 24108 * @param {boolean} [value] 24109 * - true means that we should autoplay 24110 * - false means that we should not autoplay 24111 * 24112 * @return {string} 24113 * The current value of autoplay when getting 24114 */ 24115 24116 24117 Player.prototype.autoplay = function autoplay(value) { 24118 if (value !== undefined) { 24119 this.techCall_('setAutoplay', value); 24120 this.options_.autoplay = value; 24121 return; 24122 } 24123 return this.techGet_('autoplay', value); 24124 }; 24125 24126 /** 24127 * Set or unset the playsinline attribute. 24128 * Playsinline tells the browser that non-fullscreen playback is preferred. 24129 * 24130 * @param {boolean} [value] 24131 * - true means that we should try to play inline by default 24132 * - false means that we should use the browser's default playback mode, 24133 * which in most cases is inline. iOS Safari is a notable exception 24134 * and plays fullscreen by default. 24135 * 24136 * @return {string|Player} 24137 * - the current value of playsinline 24138 * - the player when setting 24139 * 24140 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 24141 */ 24142 24143 24144 Player.prototype.playsinline = function playsinline(value) { 24145 if (value !== undefined) { 24146 this.techCall_('setPlaysinline', value); 24147 this.options_.playsinline = value; 24148 return this; 24149 } 24150 return this.techGet_('playsinline'); 24151 }; 24152 24153 /** 24154 * Get or set the loop attribute on the video element. 24155 * 24156 * @param {boolean} [value] 24157 * - true means that we should loop the video 24158 * - false means that we should not loop the video 24159 * 24160 * @return {string} 24161 * The current value of loop when getting 24162 */ 24163 24164 24165 Player.prototype.loop = function loop(value) { 24166 if (value !== undefined) { 24167 this.techCall_('setLoop', value); 24168 this.options_.loop = value; 24169 return; 24170 } 24171 return this.techGet_('loop'); 24172 }; 24173 24174 /** 24175 * Get or set the poster image source url 24176 * 24177 * @fires Player#posterchange 24178 * 24179 * @param {string} [src] 24180 * Poster image source URL 24181 * 24182 * @return {string} 24183 * The current value of poster when getting 24184 */ 24185 24186 24187 Player.prototype.poster = function poster(src) { 24188 if (src === undefined) { 24189 return this.poster_; 24190 } 24191 24192 // The correct way to remove a poster is to set as an empty string 24193 // other falsey values will throw errors 24194 if (!src) { 24195 src = ''; 24196 } 24197 24198 if (src === this.poster_) { 24199 return; 24200 } 24201 24202 // update the internal poster variable 24203 this.poster_ = src; 24204 24205 // update the tech's poster 24206 this.techCall_('setPoster', src); 24207 24208 this.isPosterFromTech_ = false; 24209 24210 // alert components that the poster has been set 24211 /** 24212 * This event fires when the poster image is changed on the player. 24213 * 24214 * @event Player#posterchange 24215 * @type {EventTarget~Event} 24216 */ 24217 this.trigger('posterchange'); 24218 }; 24219 24220 /** 24221 * Some techs (e.g. YouTube) can provide a poster source in an 24222 * asynchronous way. We want the poster component to use this 24223 * poster source so that it covers up the tech's controls. 24224 * (YouTube's play button). However we only want to use this 24225 * source if the player user hasn't set a poster through 24226 * the normal APIs. 24227 * 24228 * @fires Player#posterchange 24229 * @listens Tech#posterchange 24230 * @private 24231 */ 24232 24233 24234 Player.prototype.handleTechPosterChange_ = function handleTechPosterChange_() { 24235 if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) { 24236 var newPoster = this.tech_.poster() || ''; 24237 24238 if (newPoster !== this.poster_) { 24239 this.poster_ = newPoster; 24240 this.isPosterFromTech_ = true; 24241 24242 // Let components know the poster has changed 24243 this.trigger('posterchange'); 24244 } 24245 } 24246 }; 24247 24248 /** 24249 * Get or set whether or not the controls are showing. 24250 * 24251 * @fires Player#controlsenabled 24252 * 24253 * @param {boolean} [bool] 24254 * - true to turn controls on 24255 * - false to turn controls off 24256 * 24257 * @return {boolean} 24258 * The current value of controls when getting 24259 */ 24260 24261 24262 Player.prototype.controls = function controls(bool) { 24263 if (bool === undefined) { 24264 return !!this.controls_; 24265 } 24266 24267 bool = !!bool; 24268 24269 // Don't trigger a change event unless it actually changed 24270 if (this.controls_ === bool) { 24271 return; 24272 } 24273 24274 this.controls_ = bool; 24275 24276 if (this.usingNativeControls()) { 24277 this.techCall_('setControls', bool); 24278 } 24279 24280 if (this.controls_) { 24281 this.removeClass('vjs-controls-disabled'); 24282 this.addClass('vjs-controls-enabled'); 24283 /** 24284 * @event Player#controlsenabled 24285 * @type {EventTarget~Event} 24286 */ 24287 this.trigger('controlsenabled'); 24288 if (!this.usingNativeControls()) { 24289 this.addTechControlsListeners_(); 24290 } 24291 } else { 24292 this.removeClass('vjs-controls-enabled'); 24293 this.addClass('vjs-controls-disabled'); 24294 /** 24295 * @event Player#controlsdisabled 24296 * @type {EventTarget~Event} 24297 */ 24298 this.trigger('controlsdisabled'); 24299 if (!this.usingNativeControls()) { 24300 this.removeTechControlsListeners_(); 24301 } 24302 } 24303 }; 24304 24305 /** 24306 * Toggle native controls on/off. Native controls are the controls built into 24307 * devices (e.g. default iPhone controls), Flash, or other techs 24308 * (e.g. Vimeo Controls) 24309 * **This should only be set by the current tech, because only the tech knows 24310 * if it can support native controls** 24311 * 24312 * @fires Player#usingnativecontrols 24313 * @fires Player#usingcustomcontrols 24314 * 24315 * @param {boolean} [bool] 24316 * - true to turn native controls on 24317 * - false to turn native controls off 24318 * 24319 * @return {boolean} 24320 * The current value of native controls when getting 24321 */ 24322 24323 24324 Player.prototype.usingNativeControls = function usingNativeControls(bool) { 24325 if (bool === undefined) { 24326 return !!this.usingNativeControls_; 24327 } 24328 24329 bool = !!bool; 24330 24331 // Don't trigger a change event unless it actually changed 24332 if (this.usingNativeControls_ === bool) { 24333 return; 24334 } 24335 24336 this.usingNativeControls_ = bool; 24337 24338 if (this.usingNativeControls_) { 24339 this.addClass('vjs-using-native-controls'); 24340 24341 /** 24342 * player is using the native device controls 24343 * 24344 * @event Player#usingnativecontrols 24345 * @type {EventTarget~Event} 24346 */ 24347 this.trigger('usingnativecontrols'); 24348 } else { 24349 this.removeClass('vjs-using-native-controls'); 24350 24351 /** 24352 * player is using the custom HTML controls 24353 * 24354 * @event Player#usingcustomcontrols 24355 * @type {EventTarget~Event} 24356 */ 24357 this.trigger('usingcustomcontrols'); 24358 } 24359 }; 24360 24361 /** 24362 * Set or get the current MediaError 24363 * 24364 * @fires Player#error 24365 * 24366 * @param {MediaError|string|number} [err] 24367 * A MediaError or a string/number to be turned 24368 * into a MediaError 24369 * 24370 * @return {MediaError|null} 24371 * The current MediaError when getting (or null) 24372 */ 24373 24374 24375 Player.prototype.error = function error(err) { 24376 if (err === undefined) { 24377 return this.error_ || null; 24378 } 24379 24380 // restoring to default 24381 if (err === null) { 24382 this.error_ = err; 24383 this.removeClass('vjs-error'); 24384 if (this.errorDisplay) { 24385 this.errorDisplay.close(); 24386 } 24387 return; 24388 } 24389 24390 this.error_ = new MediaError(err); 24391 24392 // add the vjs-error classname to the player 24393 this.addClass('vjs-error'); 24394 24395 // log the name of the error type and any message 24396 // IE11 logs "[object object]" and required you to expand message to see error object 24397 log$1.error('(CODE:' + this.error_.code + ' ' + MediaError.errorTypes[this.error_.code] + ')', this.error_.message, this.error_); 24398 24399 /** 24400 * @event Player#error 24401 * @type {EventTarget~Event} 24402 */ 24403 this.trigger('error'); 24404 24405 return; 24406 }; 24407 24408 /** 24409 * Report user activity 24410 * 24411 * @param {Object} event 24412 * Event object 24413 */ 24414 24415 24416 Player.prototype.reportUserActivity = function reportUserActivity(event) { 24417 this.userActivity_ = true; 24418 }; 24419 24420 /** 24421 * Get/set if user is active 24422 * 24423 * @fires Player#useractive 24424 * @fires Player#userinactive 24425 * 24426 * @param {boolean} [bool] 24427 * - true if the user is active 24428 * - false if the user is inactive 24429 * 24430 * @return {boolean} 24431 * The current value of userActive when getting 24432 */ 24433 24434 24435 Player.prototype.userActive = function userActive(bool) { 24436 if (bool === undefined) { 24437 return this.userActive_; 24438 } 24439 24440 bool = !!bool; 24441 24442 if (bool === this.userActive_) { 24443 return; 24444 } 24445 24446 this.userActive_ = bool; 24447 24448 if (this.userActive_) { 24449 this.userActivity_ = true; 24450 this.removeClass('vjs-user-inactive'); 24451 this.addClass('vjs-user-active'); 24452 /** 24453 * @event Player#useractive 24454 * @type {EventTarget~Event} 24455 */ 24456 this.trigger('useractive'); 24457 return; 24458 } 24459 24460 // Chrome/Safari/IE have bugs where when you change the cursor it can 24461 // trigger a mousemove event. This causes an issue when you're hiding 24462 // the cursor when the user is inactive, and a mousemove signals user 24463 // activity. Making it impossible to go into inactive mode. Specifically 24464 // this happens in fullscreen when we really need to hide the cursor. 24465 // 24466 // When this gets resolved in ALL browsers it can be removed 24467 // https://code.google.com/p/chromium/issues/detail?id=103041 24468 if (this.tech_) { 24469 this.tech_.one('mousemove', function (e) { 24470 e.stopPropagation(); 24471 e.preventDefault(); 24472 }); 24473 } 24474 24475 this.userActivity_ = false; 24476 this.removeClass('vjs-user-active'); 24477 this.addClass('vjs-user-inactive'); 24478 /** 24479 * @event Player#userinactive 24480 * @type {EventTarget~Event} 24481 */ 24482 this.trigger('userinactive'); 24483 }; 24484 24485 /** 24486 * Listen for user activity based on timeout value 24487 * 24488 * @private 24489 */ 24490 24491 24492 Player.prototype.listenForUserActivity_ = function listenForUserActivity_() { 24493 var mouseInProgress = void 0; 24494 var lastMoveX = void 0; 24495 var lastMoveY = void 0; 24496 var handleActivity = bind(this, this.reportUserActivity); 24497 24498 var handleMouseMove = function handleMouseMove(e) { 24499 // #1068 - Prevent mousemove spamming 24500 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 24501 if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { 24502 lastMoveX = e.screenX; 24503 lastMoveY = e.screenY; 24504 handleActivity(); 24505 } 24506 }; 24507 24508 var handleMouseDown = function handleMouseDown() { 24509 handleActivity(); 24510 // For as long as the they are touching the device or have their mouse down, 24511 // we consider them active even if they're not moving their finger or mouse. 24512 // So we want to continue to update that they are active 24513 this.clearInterval(mouseInProgress); 24514 // Setting userActivity=true now and setting the interval to the same time 24515 // as the activityCheck interval (250) should ensure we never miss the 24516 // next activityCheck 24517 mouseInProgress = this.setInterval(handleActivity, 250); 24518 }; 24519 24520 var handleMouseUp = function handleMouseUp(event) { 24521 handleActivity(); 24522 // Stop the interval that maintains activity if the mouse/touch is down 24523 this.clearInterval(mouseInProgress); 24524 }; 24525 24526 // Any mouse movement will be considered user activity 24527 this.on('mousedown', handleMouseDown); 24528 this.on('mousemove', handleMouseMove); 24529 this.on('mouseup', handleMouseUp); 24530 24531 // Listen for keyboard navigation 24532 // Shouldn't need to use inProgress interval because of key repeat 24533 this.on('keydown', handleActivity); 24534 this.on('keyup', handleActivity); 24535 24536 // Run an interval every 250 milliseconds instead of stuffing everything into 24537 // the mousemove/touchmove function itself, to prevent performance degradation. 24538 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 24539 // then gets picked up by this loop 24540 // http://ejohn.org/blog/learning-from-twitter/ 24541 var inactivityTimeout = void 0; 24542 24543 this.setInterval(function () { 24544 // Check to see if mouse/touch activity has happened 24545 if (!this.userActivity_) { 24546 return; 24547 } 24548 24549 // Reset the activity tracker 24550 this.userActivity_ = false; 24551 24552 // If the user state was inactive, set the state to active 24553 this.userActive(true); 24554 24555 // Clear any existing inactivity timeout to start the timer over 24556 this.clearTimeout(inactivityTimeout); 24557 24558 var timeout = this.options_.inactivityTimeout; 24559 24560 if (timeout <= 0) { 24561 return; 24562 } 24563 24564 // In <timeout> milliseconds, if no more activity has occurred the 24565 // user will be considered inactive 24566 inactivityTimeout = this.setTimeout(function () { 24567 // Protect against the case where the inactivityTimeout can trigger just 24568 // before the next user activity is picked up by the activity check loop 24569 // causing a flicker 24570 if (!this.userActivity_) { 24571 this.userActive(false); 24572 } 24573 }, timeout); 24574 }, 250); 24575 }; 24576 24577 /** 24578 * Gets or sets the current playback rate. A playback rate of 24579 * 1.0 represents normal speed and 0.5 would indicate half-speed 24580 * playback, for instance. 24581 * 24582 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate 24583 * 24584 * @param {number} [rate] 24585 * New playback rate to set. 24586 * 24587 * @return {number} 24588 * The current playback rate when getting or 1.0 24589 */ 24590 24591 24592 Player.prototype.playbackRate = function playbackRate(rate) { 24593 if (rate !== undefined) { 24594 // NOTE: this.cache_.lastPlaybackRate is set from the tech handler 24595 // that is registered above 24596 this.techCall_('setPlaybackRate', rate); 24597 return; 24598 } 24599 24600 if (this.tech_ && this.tech_.featuresPlaybackRate) { 24601 return this.cache_.lastPlaybackRate || this.techGet_('playbackRate'); 24602 } 24603 return 1.0; 24604 }; 24605 24606 /** 24607 * Gets or sets the current default playback rate. A default playback rate of 24608 * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance. 24609 * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not 24610 * not the current playbackRate. 24611 * 24612 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate 24613 * 24614 * @param {number} [rate] 24615 * New default playback rate to set. 24616 * 24617 * @return {number|Player} 24618 * - The default playback rate when getting or 1.0 24619 * - the player when setting 24620 */ 24621 24622 24623 Player.prototype.defaultPlaybackRate = function defaultPlaybackRate(rate) { 24624 if (rate !== undefined) { 24625 return this.techCall_('setDefaultPlaybackRate', rate); 24626 } 24627 24628 if (this.tech_ && this.tech_.featuresPlaybackRate) { 24629 return this.techGet_('defaultPlaybackRate'); 24630 } 24631 return 1.0; 24632 }; 24633 24634 /** 24635 * Gets or sets the audio flag 24636 * 24637 * @param {boolean} bool 24638 * - true signals that this is an audio player 24639 * - false signals that this is not an audio player 24640 * 24641 * @return {boolean} 24642 * The current value of isAudio when getting 24643 */ 24644 24645 24646 Player.prototype.isAudio = function isAudio(bool) { 24647 if (bool !== undefined) { 24648 this.isAudio_ = !!bool; 24649 return; 24650 } 24651 24652 return !!this.isAudio_; 24653 }; 24654 24655 /** 24656 * A helper method for adding a {@link TextTrack} to our 24657 * {@link TextTrackList}. 24658 * 24659 * In addition to the W3C settings we allow adding additional info through options. 24660 * 24661 * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 24662 * 24663 * @param {string} [kind] 24664 * the kind of TextTrack you are adding 24665 * 24666 * @param {string} [label] 24667 * the label to give the TextTrack label 24668 * 24669 * @param {string} [language] 24670 * the language to set on the TextTrack 24671 * 24672 * @return {TextTrack|undefined} 24673 * the TextTrack that was added or undefined 24674 * if there is no tech 24675 */ 24676 24677 24678 Player.prototype.addTextTrack = function addTextTrack(kind, label, language) { 24679 if (this.tech_) { 24680 return this.tech_.addTextTrack(kind, label, language); 24681 } 24682 }; 24683 24684 /** 24685 * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}. It will 24686 * automatically removed from the video element whenever the source changes, unless 24687 * manualCleanup is set to false. 24688 * 24689 * @param {Object} options 24690 * Options to pass to {@link HTMLTrackElement} during creation. See 24691 * {@link HTMLTrackElement} for object properties that you should use. 24692 * 24693 * @param {boolean} [manualCleanup=true] if set to false, the TextTrack will be 24694 * 24695 * @return {HtmlTrackElement} 24696 * the HTMLTrackElement that was created and added 24697 * to the HtmlTrackElementList and the remote 24698 * TextTrackList 24699 * 24700 * @deprecated The default value of the "manualCleanup" parameter will default 24701 * to "false" in upcoming versions of Video.js 24702 */ 24703 24704 24705 Player.prototype.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 24706 if (this.tech_) { 24707 return this.tech_.addRemoteTextTrack(options, manualCleanup); 24708 } 24709 }; 24710 24711 /** 24712 * Remove a remote {@link TextTrack} from the respective 24713 * {@link TextTrackList} and {@link HtmlTrackElementList}. 24714 * 24715 * @param {Object} track 24716 * Remote {@link TextTrack} to remove 24717 * 24718 * @return {undefined} 24719 * does not return anything 24720 */ 24721 24722 24723 Player.prototype.removeRemoteTextTrack = function removeRemoteTextTrack() { 24724 var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 24725 _ref3$track = _ref3.track, 24726 track = _ref3$track === undefined ? arguments[0] : _ref3$track; 24727 24728 // destructure the input into an object with a track argument, defaulting to arguments[0] 24729 // default the whole argument to an empty object if nothing was passed in 24730 24731 if (this.tech_) { 24732 return this.tech_.removeRemoteTextTrack(track); 24733 } 24734 }; 24735 24736 /** 24737 * Gets available media playback quality metrics as specified by the W3C's Media 24738 * Playback Quality API. 24739 * 24740 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 24741 * 24742 * @return {Object|undefined} 24743 * An object with supported media playback quality metrics or undefined if there 24744 * is no tech or the tech does not support it. 24745 */ 24746 24747 24748 Player.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 24749 return this.techGet_('getVideoPlaybackQuality'); 24750 }; 24751 24752 /** 24753 * Get video width 24754 * 24755 * @return {number} 24756 * current video width 24757 */ 24758 24759 24760 Player.prototype.videoWidth = function videoWidth() { 24761 return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0; 24762 }; 24763 24764 /** 24765 * Get video height 24766 * 24767 * @return {number} 24768 * current video height 24769 */ 24770 24771 24772 Player.prototype.videoHeight = function videoHeight() { 24773 return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0; 24774 }; 24775 24776 /** 24777 * The player's language code 24778 * NOTE: The language should be set in the player options if you want the 24779 * the controls to be built with a specific language. Changing the language 24780 * later will not update controls text. 24781 * 24782 * @param {string} [code] 24783 * the language code to set the player to 24784 * 24785 * @return {string} 24786 * The current language code when getting 24787 */ 24788 24789 24790 Player.prototype.language = function language(code) { 24791 if (code === undefined) { 24792 return this.language_; 24793 } 24794 24795 this.language_ = String(code).toLowerCase(); 24796 }; 24797 24798 /** 24799 * Get the player's language dictionary 24800 * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time 24801 * Languages specified directly in the player options have precedence 24802 * 24803 * @return {Array} 24804 * An array of of supported languages 24805 */ 24806 24807 24808 Player.prototype.languages = function languages() { 24809 return mergeOptions(Player.prototype.options_.languages, this.languages_); 24810 }; 24811 24812 /** 24813 * returns a JavaScript object reperesenting the current track 24814 * information. **DOES not return it as JSON** 24815 * 24816 * @return {Object} 24817 * Object representing the current of track info 24818 */ 24819 24820 24821 Player.prototype.toJSON = function toJSON() { 24822 var options = mergeOptions(this.options_); 24823 var tracks = options.tracks; 24824 24825 options.tracks = []; 24826 24827 for (var i = 0; i < tracks.length; i++) { 24828 var track = tracks[i]; 24829 24830 // deep merge tracks and null out player so no circular references 24831 track = mergeOptions(track); 24832 track.player = undefined; 24833 options.tracks[i] = track; 24834 } 24835 24836 return options; 24837 }; 24838 24839 /** 24840 * Creates a simple modal dialog (an instance of the {@link ModalDialog} 24841 * component) that immediately overlays the player with arbitrary 24842 * content and removes itself when closed. 24843 * 24844 * @param {string|Function|Element|Array|null} content 24845 * Same as {@link ModalDialog#content}'s param of the same name. 24846 * The most straight-forward usage is to provide a string or DOM 24847 * element. 24848 * 24849 * @param {Object} [options] 24850 * Extra options which will be passed on to the {@link ModalDialog}. 24851 * 24852 * @return {ModalDialog} 24853 * the {@link ModalDialog} that was created 24854 */ 24855 24856 24857 Player.prototype.createModal = function createModal(content, options) { 24858 var _this10 = this; 24859 24860 options = options || {}; 24861 options.content = content || ''; 24862 24863 var modal = new ModalDialog(this, options); 24864 24865 this.addChild(modal); 24866 modal.on('dispose', function () { 24867 _this10.removeChild(modal); 24868 }); 24869 24870 modal.open(); 24871 return modal; 24872 }; 24873 24874 /** 24875 * Gets tag settings 24876 * 24877 * @param {Element} tag 24878 * The player tag 24879 * 24880 * @return {Object} 24881 * An object containing all of the settings 24882 * for a player tag 24883 */ 24884 24885 24886 Player.getTagSettings = function getTagSettings(tag) { 24887 var baseOptions = { 24888 sources: [], 24889 tracks: [] 24890 }; 24891 24892 var tagOptions = getAttributes(tag); 24893 var dataSetup = tagOptions['data-setup']; 24894 24895 if (hasClass(tag, 'vjs-fluid')) { 24896 tagOptions.fluid = true; 24897 } 24898 24899 // Check if data-setup attr exists. 24900 if (dataSetup !== null) { 24901 // Parse options JSON 24902 // If empty string, make it a parsable json object. 24903 var _safeParseTuple = tuple(dataSetup || '{}'), 24904 err = _safeParseTuple[0], 24905 data = _safeParseTuple[1]; 24906 24907 if (err) { 24908 log$1.error(err); 24909 } 24910 assign(tagOptions, data); 24911 } 24912 24913 assign(baseOptions, tagOptions); 24914 24915 // Get tag children settings 24916 if (tag.hasChildNodes()) { 24917 var children = tag.childNodes; 24918 24919 for (var i = 0, j = children.length; i < j; i++) { 24920 var child = children[i]; 24921 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 24922 var childName = child.nodeName.toLowerCase(); 24923 24924 if (childName === 'source') { 24925 baseOptions.sources.push(getAttributes(child)); 24926 } else if (childName === 'track') { 24927 baseOptions.tracks.push(getAttributes(child)); 24928 } 24929 } 24930 } 24931 24932 return baseOptions; 24933 }; 24934 24935 /** 24936 * Determine whether or not flexbox is supported 24937 * 24938 * @return {boolean} 24939 * - true if flexbox is supported 24940 * - false if flexbox is not supported 24941 */ 24942 24943 24944 Player.prototype.flexNotSupported_ = function flexNotSupported_() { 24945 var elem = document_1.createElement('i'); 24946 24947 // Note: We don't actually use flexBasis (or flexOrder), but it's one of the more 24948 // common flex features that we can rely on when checking for flex support. 24949 return !('flexBasis' in elem.style || 'webkitFlexBasis' in elem.style || 'mozFlexBasis' in elem.style || 'msFlexBasis' in elem.style || 24950 // IE10-specific (2012 flex spec), available for completeness 24951 'msFlexOrder' in elem.style); 24952 }; 24953 24954 return Player; 24955 }(Component); 24956 24957 /** 24958 * Get the {@link VideoTrackList} 24959 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist 24960 * 24961 * @return {VideoTrackList} 24962 * the current video track list 24963 * 24964 * @method Player.prototype.videoTracks 24965 */ 24966 24967 /** 24968 * Get the {@link AudioTrackList} 24969 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist 24970 * 24971 * @return {AudioTrackList} 24972 * the current audio track list 24973 * 24974 * @method Player.prototype.audioTracks 24975 */ 24976 24977 /** 24978 * Get the {@link TextTrackList} 24979 * 24980 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 24981 * 24982 * @return {TextTrackList} 24983 * the current text track list 24984 * 24985 * @method Player.prototype.textTracks 24986 */ 24987 24988 /** 24989 * Get the remote {@link TextTrackList} 24990 * 24991 * @return {TextTrackList} 24992 * The current remote text track list 24993 * 24994 * @method Player.prototype.remoteTextTracks 24995 */ 24996 24997 /** 24998 * Get the remote {@link HtmlTrackElementList} tracks. 24999 * 25000 * @return {HtmlTrackElementList} 25001 * The current remote text track element list 25002 * 25003 * @method Player.prototype.remoteTextTrackEls 25004 */ 25005 25006 ALL.names.forEach(function (name$$1) { 25007 var props = ALL[name$$1]; 25008 25009 Player.prototype[props.getterName] = function () { 25010 if (this.tech_) { 25011 return this.tech_[props.getterName](); 25012 } 25013 25014 // if we have not yet loadTech_, we create {video,audio,text}Tracks_ 25015 // these will be passed to the tech during loading 25016 this[props.privateName] = this[props.privateName] || new props.ListClass(); 25017 return this[props.privateName]; 25018 }; 25019 }); 25020 25021 /** 25022 * Global player list 25023 * 25024 * @type {Object} 25025 */ 25026 Player.players = {}; 25027 25028 var navigator = window_1.navigator; 25029 25030 /* 25031 * Player instance options, surfaced using options 25032 * options = Player.prototype.options_ 25033 * Make changes in options, not here. 25034 * 25035 * @type {Object} 25036 * @private 25037 */ 25038 Player.prototype.options_ = { 25039 // Default order of fallback technology 25040 techOrder: Tech.defaultTechOrder_, 25041 25042 html5: {}, 25043 flash: {}, 25044 25045 // default inactivity timeout 25046 inactivityTimeout: 2000, 25047 25048 // default playback rates 25049 playbackRates: [], 25050 // Add playback rate selection by adding rates 25051 // 'playbackRates': [0.5, 1, 1.5, 2], 25052 25053 // Included control sets 25054 children: ['mediaLoader', 'posterImage', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'], 25055 25056 language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en', 25057 25058 // locales and their language translations 25059 languages: {}, 25060 25061 // Default message to show when a video cannot be played. 25062 notSupportedMessage: 'No compatible source was found for this media.' 25063 }; 25064 25065 [ 25066 /** 25067 * Returns whether or not the player is in the "ended" state. 25068 * 25069 * @return {Boolean} True if the player is in the ended state, false if not. 25070 * @method Player#ended 25071 */ 25072 'ended', 25073 /** 25074 * Returns whether or not the player is in the "seeking" state. 25075 * 25076 * @return {Boolean} True if the player is in the seeking state, false if not. 25077 * @method Player#seeking 25078 */ 25079 'seeking', 25080 /** 25081 * Returns the TimeRanges of the media that are currently available 25082 * for seeking to. 25083 * 25084 * @return {TimeRanges} the seekable intervals of the media timeline 25085 * @method Player#seekable 25086 */ 25087 'seekable', 25088 /** 25089 * Returns the current state of network activity for the element, from 25090 * the codes in the list below. 25091 * - NETWORK_EMPTY (numeric value 0) 25092 * The element has not yet been initialised. All attributes are in 25093 * their initial states. 25094 * - NETWORK_IDLE (numeric value 1) 25095 * The element's resource selection algorithm is active and has 25096 * selected a resource, but it is not actually using the network at 25097 * this time. 25098 * - NETWORK_LOADING (numeric value 2) 25099 * The user agent is actively trying to download data. 25100 * - NETWORK_NO_SOURCE (numeric value 3) 25101 * The element's resource selection algorithm is active, but it has 25102 * not yet found a resource to use. 25103 * 25104 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states 25105 * @return {number} the current network activity state 25106 * @method Player#networkState 25107 */ 25108 'networkState', 25109 /** 25110 * Returns a value that expresses the current state of the element 25111 * with respect to rendering the current playback position, from the 25112 * codes in the list below. 25113 * - HAVE_NOTHING (numeric value 0) 25114 * No information regarding the media resource is available. 25115 * - HAVE_METADATA (numeric value 1) 25116 * Enough of the resource has been obtained that the duration of the 25117 * resource is available. 25118 * - HAVE_CURRENT_DATA (numeric value 2) 25119 * Data for the immediate current playback position is available. 25120 * - HAVE_FUTURE_DATA (numeric value 3) 25121 * Data for the immediate current playback position is available, as 25122 * well as enough data for the user agent to advance the current 25123 * playback position in the direction of playback. 25124 * - HAVE_ENOUGH_DATA (numeric value 4) 25125 * The user agent estimates that enough data is available for 25126 * playback to proceed uninterrupted. 25127 * 25128 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate 25129 * @return {number} the current playback rendering state 25130 * @method Player#readyState 25131 */ 25132 'readyState'].forEach(function (fn) { 25133 Player.prototype[fn] = function () { 25134 return this.techGet_(fn); 25135 }; 25136 }); 25137 25138 TECH_EVENTS_RETRIGGER.forEach(function (event) { 25139 Player.prototype['handleTech' + toTitleCase(event) + '_'] = function () { 25140 return this.trigger(event); 25141 }; 25142 }); 25143 25144 /** 25145 * Fired when the player has initial duration and dimension information 25146 * 25147 * @event Player#loadedmetadata 25148 * @type {EventTarget~Event} 25149 */ 25150 25151 /** 25152 * Fired when the player has downloaded data at the current playback position 25153 * 25154 * @event Player#loadeddata 25155 * @type {EventTarget~Event} 25156 */ 25157 25158 /** 25159 * Fired when the current playback position has changed * 25160 * During playback this is fired every 15-250 milliseconds, depending on the 25161 * playback technology in use. 25162 * 25163 * @event Player#timeupdate 25164 * @type {EventTarget~Event} 25165 */ 25166 25167 /** 25168 * Fired when the volume changes 25169 * 25170 * @event Player#volumechange 25171 * @type {EventTarget~Event} 25172 */ 25173 25174 /** 25175 * Reports whether or not a player has a plugin available. 25176 * 25177 * This does not report whether or not the plugin has ever been initialized 25178 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}. 25179 * 25180 * @method Player#hasPlugin 25181 * @param {string} name 25182 * The name of a plugin. 25183 * 25184 * @return {boolean} 25185 * Whether or not this player has the requested plugin available. 25186 */ 25187 25188 /** 25189 * Reports whether or not a player is using a plugin by name. 25190 * 25191 * For basic plugins, this only reports whether the plugin has _ever_ been 25192 * initialized on this player. 25193 * 25194 * @method Player#usingPlugin 25195 * @param {string} name 25196 * The name of a plugin. 25197 * 25198 * @return {boolean} 25199 * Whether or not this player is using the requested plugin. 25200 */ 25201 25202 Component.registerComponent('Player', Player); 25203 25204 /** 25205 * @file plugin.js 25206 */ 25207 25208 /** 25209 * The base plugin name. 25210 * 25211 * @private 25212 * @constant 25213 * @type {string} 25214 */ 25215 var BASE_PLUGIN_NAME = 'plugin'; 25216 25217 /** 25218 * The key on which a player's active plugins cache is stored. 25219 * 25220 * @private 25221 * @constant 25222 * @type {string} 25223 */ 25224 var PLUGIN_CACHE_KEY = 'activePlugins_'; 25225 25226 /** 25227 * Stores registered plugins in a private space. 25228 * 25229 * @private 25230 * @type {Object} 25231 */ 25232 var pluginStorage = {}; 25233 25234 /** 25235 * Reports whether or not a plugin has been registered. 25236 * 25237 * @private 25238 * @param {string} name 25239 * The name of a plugin. 25240 * 25241 * @returns {boolean} 25242 * Whether or not the plugin has been registered. 25243 */ 25244 var pluginExists = function pluginExists(name) { 25245 return pluginStorage.hasOwnProperty(name); 25246 }; 25247 25248 /** 25249 * Get a single registered plugin by name. 25250 * 25251 * @private 25252 * @param {string} name 25253 * The name of a plugin. 25254 * 25255 * @returns {Function|undefined} 25256 * The plugin (or undefined). 25257 */ 25258 var getPlugin = function getPlugin(name) { 25259 return pluginExists(name) ? pluginStorage[name] : undefined; 25260 }; 25261 25262 /** 25263 * Marks a plugin as "active" on a player. 25264 * 25265 * Also, ensures that the player has an object for tracking active plugins. 25266 * 25267 * @private 25268 * @param {Player} player 25269 * A Video.js player instance. 25270 * 25271 * @param {string} name 25272 * The name of a plugin. 25273 */ 25274 var markPluginAsActive = function markPluginAsActive(player, name) { 25275 player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {}; 25276 player[PLUGIN_CACHE_KEY][name] = true; 25277 }; 25278 25279 /** 25280 * Triggers a pair of plugin setup events. 25281 * 25282 * @private 25283 * @param {Player} player 25284 * A Video.js player instance. 25285 * 25286 * @param {Plugin~PluginEventHash} hash 25287 * A plugin event hash. 25288 * 25289 * @param {Boolean} [before] 25290 * If true, prefixes the event name with "before". In other words, 25291 * use this to trigger "beforepluginsetup" instead of "pluginsetup". 25292 */ 25293 var triggerSetupEvent = function triggerSetupEvent(player, hash, before) { 25294 var eventName = (before ? 'before' : '') + 'pluginsetup'; 25295 25296 player.trigger(eventName, hash); 25297 player.trigger(eventName + ':' + hash.name, hash); 25298 }; 25299 25300 /** 25301 * Takes a basic plugin function and returns a wrapper function which marks 25302 * on the player that the plugin has been activated. 25303 * 25304 * @private 25305 * @param {string} name 25306 * The name of the plugin. 25307 * 25308 * @param {Function} plugin 25309 * The basic plugin. 25310 * 25311 * @returns {Function} 25312 * A wrapper function for the given plugin. 25313 */ 25314 var createBasicPlugin = function createBasicPlugin(name, plugin) { 25315 var basicPluginWrapper = function basicPluginWrapper() { 25316 25317 // We trigger the "beforepluginsetup" and "pluginsetup" events on the player 25318 // regardless, but we want the hash to be consistent with the hash provided 25319 // for advanced plugins. 25320 // 25321 // The only potentially counter-intuitive thing here is the `instance` in 25322 // the "pluginsetup" event is the value returned by the `plugin` function. 25323 triggerSetupEvent(this, { name: name, plugin: plugin, instance: null }, true); 25324 25325 var instance = plugin.apply(this, arguments); 25326 25327 markPluginAsActive(this, name); 25328 triggerSetupEvent(this, { name: name, plugin: plugin, instance: instance }); 25329 25330 return instance; 25331 }; 25332 25333 Object.keys(plugin).forEach(function (prop) { 25334 basicPluginWrapper[prop] = plugin[prop]; 25335 }); 25336 25337 return basicPluginWrapper; 25338 }; 25339 25340 /** 25341 * Takes a plugin sub-class and returns a factory function for generating 25342 * instances of it. 25343 * 25344 * This factory function will replace itself with an instance of the requested 25345 * sub-class of Plugin. 25346 * 25347 * @private 25348 * @param {string} name 25349 * The name of the plugin. 25350 * 25351 * @param {Plugin} PluginSubClass 25352 * The advanced plugin. 25353 * 25354 * @returns {Function} 25355 */ 25356 var createPluginFactory = function createPluginFactory(name, PluginSubClass) { 25357 25358 // Add a `name` property to the plugin prototype so that each plugin can 25359 // refer to itself by name. 25360 PluginSubClass.prototype.name = name; 25361 25362 return function () { 25363 triggerSetupEvent(this, { name: name, plugin: PluginSubClass, instance: null }, true); 25364 25365 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 25366 args[_key] = arguments[_key]; 25367 } 25368 25369 var instance = new (Function.prototype.bind.apply(PluginSubClass, [null].concat([this].concat(args))))(); 25370 25371 // The plugin is replaced by a function that returns the current instance. 25372 this[name] = function () { 25373 return instance; 25374 }; 25375 25376 triggerSetupEvent(this, instance.getEventHash()); 25377 25378 return instance; 25379 }; 25380 }; 25381 25382 /** 25383 * Parent class for all advanced plugins. 25384 * 25385 * @mixes module:evented~EventedMixin 25386 * @mixes module:stateful~StatefulMixin 25387 * @fires Player#beforepluginsetup 25388 * @fires Player#beforepluginsetup:$name 25389 * @fires Player#pluginsetup 25390 * @fires Player#pluginsetup:$name 25391 * @listens Player#dispose 25392 * @throws {Error} 25393 * If attempting to instantiate the base {@link Plugin} class 25394 * directly instead of via a sub-class. 25395 */ 25396 25397 var Plugin = function () { 25398 25399 /** 25400 * Creates an instance of this class. 25401 * 25402 * Sub-classes should call `super` to ensure plugins are properly initialized. 25403 * 25404 * @param {Player} player 25405 * A Video.js player instance. 25406 */ 25407 function Plugin(player) { 25408 classCallCheck(this, Plugin); 25409 25410 if (this.constructor === Plugin) { 25411 throw new Error('Plugin must be sub-classed; not directly instantiated.'); 25412 } 25413 25414 this.player = player; 25415 25416 // Make this object evented, but remove the added `trigger` method so we 25417 // use the prototype version instead. 25418 evented(this); 25419 delete this.trigger; 25420 25421 stateful(this, this.constructor.defaultState); 25422 markPluginAsActive(player, this.name); 25423 25424 // Auto-bind the dispose method so we can use it as a listener and unbind 25425 // it later easily. 25426 this.dispose = bind(this, this.dispose); 25427 25428 // If the player is disposed, dispose the plugin. 25429 player.on('dispose', this.dispose); 25430 } 25431 25432 /** 25433 * Get the version of the plugin that was set on <pluginName>.VERSION 25434 */ 25435 25436 25437 Plugin.prototype.version = function version() { 25438 return this.constructor.VERSION; 25439 }; 25440 25441 /** 25442 * Each event triggered by plugins includes a hash of additional data with 25443 * conventional properties. 25444 * 25445 * This returns that object or mutates an existing hash. 25446 * 25447 * @param {Object} [hash={}] 25448 * An object to be used as event an event hash. 25449 * 25450 * @returns {Plugin~PluginEventHash} 25451 * An event hash object with provided properties mixed-in. 25452 */ 25453 25454 25455 Plugin.prototype.getEventHash = function getEventHash() { 25456 var hash = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 25457 25458 hash.name = this.name; 25459 hash.plugin = this.constructor; 25460 hash.instance = this; 25461 return hash; 25462 }; 25463 25464 /** 25465 * Triggers an event on the plugin object and overrides 25466 * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}. 25467 * 25468 * @param {string|Object} event 25469 * An event type or an object with a type property. 25470 * 25471 * @param {Object} [hash={}] 25472 * Additional data hash to merge with a 25473 * {@link Plugin~PluginEventHash|PluginEventHash}. 25474 * 25475 * @returns {boolean} 25476 * Whether or not default was prevented. 25477 */ 25478 25479 25480 Plugin.prototype.trigger = function trigger$$1(event) { 25481 var hash = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 25482 25483 return trigger(this.eventBusEl_, event, this.getEventHash(hash)); 25484 }; 25485 25486 /** 25487 * Handles "statechanged" events on the plugin. No-op by default, override by 25488 * subclassing. 25489 * 25490 * @abstract 25491 * @param {Event} e 25492 * An event object provided by a "statechanged" event. 25493 * 25494 * @param {Object} e.changes 25495 * An object describing changes that occurred with the "statechanged" 25496 * event. 25497 */ 25498 25499 25500 Plugin.prototype.handleStateChanged = function handleStateChanged(e) {}; 25501 25502 /** 25503 * Disposes a plugin. 25504 * 25505 * Subclasses can override this if they want, but for the sake of safety, 25506 * it's probably best to subscribe the "dispose" event. 25507 * 25508 * @fires Plugin#dispose 25509 */ 25510 25511 25512 Plugin.prototype.dispose = function dispose() { 25513 var name = this.name, 25514 player = this.player; 25515 25516 /** 25517 * Signals that a advanced plugin is about to be disposed. 25518 * 25519 * @event Plugin#dispose 25520 * @type {EventTarget~Event} 25521 */ 25522 25523 this.trigger('dispose'); 25524 this.off(); 25525 player.off('dispose', this.dispose); 25526 25527 // Eliminate any possible sources of leaking memory by clearing up 25528 // references between the player and the plugin instance and nulling out 25529 // the plugin's state and replacing methods with a function that throws. 25530 player[PLUGIN_CACHE_KEY][name] = false; 25531 this.player = this.state = null; 25532 25533 // Finally, replace the plugin name on the player with a new factory 25534 // function, so that the plugin is ready to be set up again. 25535 player[name] = createPluginFactory(name, pluginStorage[name]); 25536 }; 25537 25538 /** 25539 * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`). 25540 * 25541 * @param {string|Function} plugin 25542 * If a string, matches the name of a plugin. If a function, will be 25543 * tested directly. 25544 * 25545 * @returns {boolean} 25546 * Whether or not a plugin is a basic plugin. 25547 */ 25548 25549 25550 Plugin.isBasic = function isBasic(plugin) { 25551 var p = typeof plugin === 'string' ? getPlugin(plugin) : plugin; 25552 25553 return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype); 25554 }; 25555 25556 /** 25557 * Register a Video.js plugin. 25558 * 25559 * @param {string} name 25560 * The name of the plugin to be registered. Must be a string and 25561 * must not match an existing plugin or a method on the `Player` 25562 * prototype. 25563 * 25564 * @param {Function} plugin 25565 * A sub-class of `Plugin` or a function for basic plugins. 25566 * 25567 * @returns {Function} 25568 * For advanced plugins, a factory function for that plugin. For 25569 * basic plugins, a wrapper function that initializes the plugin. 25570 */ 25571 25572 25573 Plugin.registerPlugin = function registerPlugin(name, plugin) { 25574 if (typeof name !== 'string') { 25575 throw new Error('Illegal plugin name, "' + name + '", must be a string, was ' + (typeof name === 'undefined' ? 'undefined' : _typeof(name)) + '.'); 25576 } 25577 25578 if (pluginExists(name)) { 25579 log$1.warn('A plugin named "' + name + '" already exists. You may want to avoid re-registering plugins!'); 25580 } else if (Player.prototype.hasOwnProperty(name)) { 25581 throw new Error('Illegal plugin name, "' + name + '", cannot share a name with an existing player method!'); 25582 } 25583 25584 if (typeof plugin !== 'function') { 25585 throw new Error('Illegal plugin for "' + name + '", must be a function, was ' + (typeof plugin === 'undefined' ? 'undefined' : _typeof(plugin)) + '.'); 25586 } 25587 25588 pluginStorage[name] = plugin; 25589 25590 // Add a player prototype method for all sub-classed plugins (but not for 25591 // the base Plugin class). 25592 if (name !== BASE_PLUGIN_NAME) { 25593 if (Plugin.isBasic(plugin)) { 25594 Player.prototype[name] = createBasicPlugin(name, plugin); 25595 } else { 25596 Player.prototype[name] = createPluginFactory(name, plugin); 25597 } 25598 } 25599 25600 return plugin; 25601 }; 25602 25603 /** 25604 * De-register a Video.js plugin. 25605 * 25606 * @param {string} name 25607 * The name of the plugin to be deregistered. 25608 */ 25609 25610 25611 Plugin.deregisterPlugin = function deregisterPlugin(name) { 25612 if (name === BASE_PLUGIN_NAME) { 25613 throw new Error('Cannot de-register base plugin.'); 25614 } 25615 if (pluginExists(name)) { 25616 delete pluginStorage[name]; 25617 delete Player.prototype[name]; 25618 } 25619 }; 25620 25621 /** 25622 * Gets an object containing multiple Video.js plugins. 25623 * 25624 * @param {Array} [names] 25625 * If provided, should be an array of plugin names. Defaults to _all_ 25626 * plugin names. 25627 * 25628 * @returns {Object|undefined} 25629 * An object containing plugin(s) associated with their name(s) or 25630 * `undefined` if no matching plugins exist). 25631 */ 25632 25633 25634 Plugin.getPlugins = function getPlugins() { 25635 var names = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Object.keys(pluginStorage); 25636 25637 var result = void 0; 25638 25639 names.forEach(function (name) { 25640 var plugin = getPlugin(name); 25641 25642 if (plugin) { 25643 result = result || {}; 25644 result[name] = plugin; 25645 } 25646 }); 25647 25648 return result; 25649 }; 25650 25651 /** 25652 * Gets a plugin's version, if available 25653 * 25654 * @param {string} name 25655 * The name of a plugin. 25656 * 25657 * @returns {string} 25658 * The plugin's version or an empty string. 25659 */ 25660 25661 25662 Plugin.getPluginVersion = function getPluginVersion(name) { 25663 var plugin = getPlugin(name); 25664 25665 return plugin && plugin.VERSION || ''; 25666 }; 25667 25668 return Plugin; 25669 }(); 25670 25671 /** 25672 * Gets a plugin by name if it exists. 25673 * 25674 * @static 25675 * @method getPlugin 25676 * @memberOf Plugin 25677 * @param {string} name 25678 * The name of a plugin. 25679 * 25680 * @returns {Function|undefined} 25681 * The plugin (or `undefined`). 25682 */ 25683 25684 25685 Plugin.getPlugin = getPlugin; 25686 25687 /** 25688 * The name of the base plugin class as it is registered. 25689 * 25690 * @type {string} 25691 */ 25692 Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME; 25693 25694 Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin); 25695 25696 /** 25697 * Documented in player.js 25698 * 25699 * @ignore 25700 */ 25701 Player.prototype.usingPlugin = function (name) { 25702 return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true; 25703 }; 25704 25705 /** 25706 * Documented in player.js 25707 * 25708 * @ignore 25709 */ 25710 Player.prototype.hasPlugin = function (name) { 25711 return !!pluginExists(name); 25712 }; 25713 25714 /** 25715 * @file extend.js 25716 * @module extend 25717 */ 25718 25719 /** 25720 * A combination of node inherits and babel's inherits (after transpile). 25721 * Both work the same but node adds `super_` to the subClass 25722 * and Bable adds the superClass as __proto__. Both seem useful. 25723 * 25724 * @param {Object} subClass 25725 * The class to inherit to 25726 * 25727 * @param {Object} superClass 25728 * The class to inherit from 25729 * 25730 * @private 25731 */ 25732 var _inherits = function _inherits(subClass, superClass) { 25733 if (typeof superClass !== 'function' && superClass !== null) { 25734 throw new TypeError('Super expression must either be null or a function, not ' + (typeof superClass === 'undefined' ? 'undefined' : _typeof(superClass))); 25735 } 25736 25737 subClass.prototype = Object.create(superClass && superClass.prototype, { 25738 constructor: { 25739 value: subClass, 25740 enumerable: false, 25741 writable: true, 25742 configurable: true 25743 } 25744 }); 25745 25746 if (superClass) { 25747 // node 25748 subClass.super_ = superClass; 25749 } 25750 }; 25751 25752 /** 25753 * Function for subclassing using the same inheritance that 25754 * videojs uses internally 25755 * 25756 * @static 25757 * @const 25758 * 25759 * @param {Object} superClass 25760 * The class to inherit from 25761 * 25762 * @param {Object} [subClassMethods={}] 25763 * The class to inherit to 25764 * 25765 * @return {Object} 25766 * The new object with subClassMethods that inherited superClass. 25767 */ 25768 var extendFn = function extendFn(superClass) { 25769 var subClassMethods = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 25770 25771 var subClass = function subClass() { 25772 superClass.apply(this, arguments); 25773 }; 25774 25775 var methods = {}; 25776 25777 if ((typeof subClassMethods === 'undefined' ? 'undefined' : _typeof(subClassMethods)) === 'object') { 25778 if (subClassMethods.constructor !== Object.prototype.constructor) { 25779 subClass = subClassMethods.constructor; 25780 } 25781 methods = subClassMethods; 25782 } else if (typeof subClassMethods === 'function') { 25783 subClass = subClassMethods; 25784 } 25785 25786 _inherits(subClass, superClass); 25787 25788 // Extend subObj's prototype with functions and other properties from props 25789 for (var name in methods) { 25790 if (methods.hasOwnProperty(name)) { 25791 subClass.prototype[name] = methods[name]; 25792 } 25793 } 25794 25795 return subClass; 25796 }; 25797 25798 /** 25799 * @file video.js 25800 * @module videojs 25801 */ 25802 25803 /** 25804 * Normalize an `id` value by trimming off a leading `#` 25805 * 25806 * @param {string} id 25807 * A string, maybe with a leading `#`. 25808 * 25809 * @returns {string} 25810 * The string, without any leading `#`. 25811 */ 25812 var normalizeId = function normalizeId(id) { 25813 return id.indexOf('#') === 0 ? id.slice(1) : id; 25814 }; 25815 25816 /** 25817 * Doubles as the main function for users to create a player instance and also 25818 * the main library object. 25819 * The `videojs` function can be used to initialize or retrieve a player. 25820 * 25821 * @param {string|Element} id 25822 * Video element or video element ID 25823 * 25824 * @param {Object} [options] 25825 * Optional options object for config/settings 25826 * 25827 * @param {Component~ReadyCallback} [ready] 25828 * Optional ready callback 25829 * 25830 * @return {Player} 25831 * A player instance 25832 */ 25833 function videojs$1(id, options, ready) { 25834 var player = videojs$1.getPlayer(id); 25835 25836 if (player) { 25837 if (options) { 25838 log$1.warn('Player "' + id + '" is already initialised. Options will not be applied.'); 25839 } 25840 if (ready) { 25841 player.ready(ready); 25842 } 25843 return player; 25844 } 25845 25846 var el = typeof id === 'string' ? $('#' + normalizeId(id)) : id; 25847 25848 if (!isEl(el)) { 25849 throw new TypeError('The element or ID supplied is not valid. (videojs)'); 25850 } 25851 25852 if (!document_1.body.contains(el)) { 25853 log$1.warn('The element supplied is not included in the DOM'); 25854 } 25855 25856 options = options || {}; 25857 25858 videojs$1.hooks('beforesetup').forEach(function (hookFunction) { 25859 var opts = hookFunction(el, mergeOptions(options)); 25860 25861 if (!isObject(opts) || Array.isArray(opts)) { 25862 log$1.error('please return an object in beforesetup hooks'); 25863 return; 25864 } 25865 25866 options = mergeOptions(options, opts); 25867 }); 25868 25869 // We get the current "Player" component here in case an integration has 25870 // replaced it with a custom player. 25871 var PlayerComponent = Component.getComponent('Player'); 25872 25873 player = new PlayerComponent(el, options, ready); 25874 25875 videojs$1.hooks('setup').forEach(function (hookFunction) { 25876 return hookFunction(player); 25877 }); 25878 25879 return player; 25880 } 25881 25882 /** 25883 * An Object that contains lifecycle hooks as keys which point to an array 25884 * of functions that are run when a lifecycle is triggered 25885 */ 25886 videojs$1.hooks_ = {}; 25887 25888 /** 25889 * Get a list of hooks for a specific lifecycle 25890 * @function videojs.hooks 25891 * 25892 * @param {string} type 25893 * the lifecyle to get hooks from 25894 * 25895 * @param {Function|Function[]} [fn] 25896 * Optionally add a hook (or hooks) to the lifecycle that your are getting. 25897 * 25898 * @return {Array} 25899 * an array of hooks, or an empty array if there are none. 25900 */ 25901 videojs$1.hooks = function (type, fn) { 25902 videojs$1.hooks_[type] = videojs$1.hooks_[type] || []; 25903 if (fn) { 25904 videojs$1.hooks_[type] = videojs$1.hooks_[type].concat(fn); 25905 } 25906 return videojs$1.hooks_[type]; 25907 }; 25908 25909 /** 25910 * Add a function hook to a specific videojs lifecycle. 25911 * 25912 * @param {string} type 25913 * the lifecycle to hook the function to. 25914 * 25915 * @param {Function|Function[]} 25916 * The function or array of functions to attach. 25917 */ 25918 videojs$1.hook = function (type, fn) { 25919 videojs$1.hooks(type, fn); 25920 }; 25921 25922 /** 25923 * Add a function hook that will only run once to a specific videojs lifecycle. 25924 * 25925 * @param {string} type 25926 * the lifecycle to hook the function to. 25927 * 25928 * @param {Function|Function[]} 25929 * The function or array of functions to attach. 25930 */ 25931 videojs$1.hookOnce = function (type, fn) { 25932 videojs$1.hooks(type, [].concat(fn).map(function (original) { 25933 var wrapper = function wrapper() { 25934 videojs$1.removeHook(type, wrapper); 25935 return original.apply(undefined, arguments); 25936 }; 25937 25938 return wrapper; 25939 })); 25940 }; 25941 25942 /** 25943 * Remove a hook from a specific videojs lifecycle. 25944 * 25945 * @param {string} type 25946 * the lifecycle that the function hooked to 25947 * 25948 * @param {Function} fn 25949 * The hooked function to remove 25950 * 25951 * @return {boolean} 25952 * The function that was removed or undef 25953 */ 25954 videojs$1.removeHook = function (type, fn) { 25955 var index = videojs$1.hooks(type).indexOf(fn); 25956 25957 if (index <= -1) { 25958 return false; 25959 } 25960 25961 videojs$1.hooks_[type] = videojs$1.hooks_[type].slice(); 25962 videojs$1.hooks_[type].splice(index, 1); 25963 25964 return true; 25965 }; 25966 25967 // Add default styles 25968 if (window_1.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) { 25969 var style$1 = $('.vjs-styles-defaults'); 25970 25971 if (!style$1) { 25972 style$1 = createStyleElement('vjs-styles-defaults'); 25973 var head = $('head'); 25974 25975 if (head) { 25976 head.insertBefore(style$1, head.firstChild); 25977 } 25978 setTextContent(style$1, '\n .video-js {\n width: 300px;\n height: 150px;\n }\n\n .vjs-fluid {\n padding-top: 56.25%\n }\n '); 25979 } 25980 } 25981 25982 // Run Auto-load players 25983 // You have to wait at least once in case this script is loaded after your 25984 // video in the DOM (weird behavior only with minified version) 25985 autoSetupTimeout(1, videojs$1); 25986 25987 /** 25988 * Current software version. Follows semver. 25989 * 25990 * @type {string} 25991 */ 25992 videojs$1.VERSION = version; 25993 25994 /** 25995 * The global options object. These are the settings that take effect 25996 * if no overrides are specified when the player is created. 25997 * 25998 * @type {Object} 25999 */ 26000 videojs$1.options = Player.prototype.options_; 26001 26002 /** 26003 * Get an object with the currently created players, keyed by player ID 26004 * 26005 * @return {Object} 26006 * The created players 26007 */ 26008 videojs$1.getPlayers = function () { 26009 return Player.players; 26010 }; 26011 26012 /** 26013 * Get a single player based on an ID or DOM element. 26014 * 26015 * This is useful if you want to check if an element or ID has an associated 26016 * Video.js player, but not create one if it doesn't. 26017 * 26018 * @param {string|Element} id 26019 * An HTML element - `<video>`, `<audio>`, or `<video-js>` - 26020 * or a string matching the `id` of such an element. 26021 * 26022 * @returns {Player|undefined} 26023 * A player instance or `undefined` if there is no player instance 26024 * matching the argument. 26025 */ 26026 videojs$1.getPlayer = function (id) { 26027 var players = Player.players; 26028 var tag = void 0; 26029 26030 if (typeof id === 'string') { 26031 var nId = normalizeId(id); 26032 var player = players[nId]; 26033 26034 if (player) { 26035 return player; 26036 } 26037 26038 tag = $('#' + nId); 26039 } else { 26040 tag = id; 26041 } 26042 26043 if (isEl(tag)) { 26044 var _tag = tag, 26045 _player = _tag.player, 26046 playerId = _tag.playerId; 26047 26048 // Element may have a `player` property referring to an already created 26049 // player instance. If so, return that. 26050 26051 if (_player || players[playerId]) { 26052 return _player || players[playerId]; 26053 } 26054 } 26055 }; 26056 26057 /** 26058 * Returns an array of all current players. 26059 * 26060 * @return {Array} 26061 * An array of all players. The array will be in the order that 26062 * `Object.keys` provides, which could potentially vary between 26063 * JavaScript engines. 26064 * 26065 */ 26066 videojs$1.getAllPlayers = function () { 26067 return ( 26068 26069 // Disposed players leave a key with a `null` value, so we need to make sure 26070 // we filter those out. 26071 Object.keys(Player.players).map(function (k) { 26072 return Player.players[k]; 26073 }).filter(Boolean) 26074 ); 26075 }; 26076 26077 /** 26078 * Expose players object. 26079 * 26080 * @memberOf videojs 26081 * @property {Object} players 26082 */ 26083 videojs$1.players = Player.players; 26084 26085 /** 26086 * Get a component class object by name 26087 * 26088 * @borrows Component.getComponent as videojs.getComponent 26089 */ 26090 videojs$1.getComponent = Component.getComponent; 26091 26092 /** 26093 * Register a component so it can referred to by name. Used when adding to other 26094 * components, either through addChild `component.addChild('myComponent')` or through 26095 * default children options `{ children: ['myComponent'] }`. 26096 * 26097 * > NOTE: You could also just initialize the component before adding. 26098 * `component.addChild(new MyComponent());` 26099 * 26100 * @param {string} name 26101 * The class name of the component 26102 * 26103 * @param {Component} comp 26104 * The component class 26105 * 26106 * @return {Component} 26107 * The newly registered component 26108 */ 26109 videojs$1.registerComponent = function (name$$1, comp) { 26110 if (Tech.isTech(comp)) { 26111 log$1.warn('The ' + name$$1 + ' tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)'); 26112 } 26113 26114 Component.registerComponent.call(Component, name$$1, comp); 26115 }; 26116 26117 /** 26118 * Get a Tech class object by name 26119 * 26120 * @borrows Tech.getTech as videojs.getTech 26121 */ 26122 videojs$1.getTech = Tech.getTech; 26123 26124 /** 26125 * Register a Tech so it can referred to by name. 26126 * This is used in the tech order for the player. 26127 * 26128 * @borrows Tech.registerTech as videojs.registerTech 26129 */ 26130 videojs$1.registerTech = Tech.registerTech; 26131 26132 /** 26133 * Register a middleware to a source type. 26134 * 26135 * @param {String} type A string representing a MIME type. 26136 * @param {function(player):object} middleware A middleware factory that takes a player. 26137 */ 26138 videojs$1.use = use; 26139 26140 /** 26141 * An object that can be returned by a middleware to signify 26142 * that the middleware is being terminated. 26143 * 26144 * @type {object} 26145 * @memberOf {videojs} 26146 * @property {object} middleware.TERMINATOR 26147 */ 26148 Object.defineProperty(videojs$1, 'middleware', { 26149 value: {}, 26150 writeable: false, 26151 enumerable: true 26152 }); 26153 26154 Object.defineProperty(videojs$1.middleware, 'TERMINATOR', { 26155 value: TERMINATOR, 26156 writeable: false, 26157 enumerable: true 26158 }); 26159 26160 /** 26161 * A suite of browser and device tests from {@link browser}. 26162 * 26163 * @type {Object} 26164 * @private 26165 */ 26166 videojs$1.browser = browser; 26167 26168 /** 26169 * Whether or not the browser supports touch events. Included for backward 26170 * compatibility with 4.x, but deprecated. Use `videojs.browser.TOUCH_ENABLED` 26171 * instead going forward. 26172 * 26173 * @deprecated since version 5.0 26174 * @type {boolean} 26175 */ 26176 videojs$1.TOUCH_ENABLED = TOUCH_ENABLED; 26177 26178 /** 26179 * Subclass an existing class 26180 * Mimics ES6 subclassing with the `extend` keyword 26181 * 26182 * @borrows extend:extendFn as videojs.extend 26183 */ 26184 videojs$1.extend = extendFn; 26185 26186 /** 26187 * Merge two options objects recursively 26188 * Performs a deep merge like lodash.merge but **only merges plain objects** 26189 * (not arrays, elements, anything else) 26190 * Other values will be copied directly from the second object. 26191 * 26192 * @borrows merge-options:mergeOptions as videojs.mergeOptions 26193 */ 26194 videojs$1.mergeOptions = mergeOptions; 26195 26196 /** 26197 * Change the context (this) of a function 26198 * 26199 * > NOTE: as of v5.0 we require an ES5 shim, so you should use the native 26200 * `function() {}.bind(newContext);` instead of this. 26201 * 26202 * @borrows fn:bind as videojs.bind 26203 */ 26204 videojs$1.bind = bind; 26205 26206 /** 26207 * Register a Video.js plugin. 26208 * 26209 * @borrows plugin:registerPlugin as videojs.registerPlugin 26210 * @method registerPlugin 26211 * 26212 * @param {string} name 26213 * The name of the plugin to be registered. Must be a string and 26214 * must not match an existing plugin or a method on the `Player` 26215 * prototype. 26216 * 26217 * @param {Function} plugin 26218 * A sub-class of `Plugin` or a function for basic plugins. 26219 * 26220 * @return {Function} 26221 * For advanced plugins, a factory function for that plugin. For 26222 * basic plugins, a wrapper function that initializes the plugin. 26223 */ 26224 videojs$1.registerPlugin = Plugin.registerPlugin; 26225 26226 /** 26227 * Deprecated method to register a plugin with Video.js 26228 * 26229 * @deprecated 26230 * videojs.plugin() is deprecated; use videojs.registerPlugin() instead 26231 * 26232 * @param {string} name 26233 * The plugin name 26234 * 26235 * @param {Plugin|Function} plugin 26236 * The plugin sub-class or function 26237 */ 26238 videojs$1.plugin = function (name$$1, plugin) { 26239 log$1.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead'); 26240 return Plugin.registerPlugin(name$$1, plugin); 26241 }; 26242 26243 /**
26244 * Gets an object containing multiple Video.js plugins. 26245 * 26246 * @param {Array} [names] 26247 * If provided, should be an array of plugin names. Defaults to _all_ 26248 * plugin names. 26249 * 26250 * @return {Object|undefined} 26251 * An object containing plugin(s) associated with their name(s) or 26252 * `undefined` if no matching plugins exist). 26253 */ 26254 videojs$1.getPlugins = Plugin.getPlugins; 26255 26256 /** 26257 * Gets a plugin by name if it exists. 26258 * 26259 * @param {string} name 26260 * The name of a plugin. 26261 * 26262 * @return {Function|undefined} 26263 * The plugin (or `undefined`). 26264 */ 26265 videojs$1.getPlugin = Plugin.getPlugin; 26266 26267 /** 26268 * Gets a plugin's version, if available 26269 * 26270 * @param {string} name 26271 * The name of a plugin. 26272 * 26273 * @return {string} 26274 * The plugin's version or an empty string. 26275 */ 26276 videojs$1.getPluginVersion = Plugin.getPluginVersion; 26277 26278 /** 26279 * Adding languages so that they're available to all players. 26280 * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });` 26281 * 26282 * @param {string} code 26283 * The language code or dictionary property 26284 * 26285 * @param {Object} data 26286 * The data values to be translated 26287 * 26288 * @return {Object} 26289 * The resulting language dictionary object 26290 */ 26291 videojs$1.addLanguage = function (code, data) { 26292 var _mergeOptions; 26293 26294 code = ('' + code).toLowerCase(); 26295 26296 videojs$1.options.languages = mergeOptions(videojs$1.options.languages, (_mergeOptions = {}, _mergeOptions[code] = data, _mergeOptions)); 26297 26298 return videojs$1.options.languages[code]; 26299 }; 26300 26301 /** 26302 * Log messages 26303 * 26304 * @borrows log:log as videojs.log 26305 */ 26306 videojs$1.log = log$1; 26307 26308 /** 26309 * Creates an emulated TimeRange object. 26310 * 26311 * @borrows time-ranges:createTimeRanges as videojs.createTimeRange 26312 */ 26313 /** 26314 * @borrows time-ranges:createTimeRanges as videojs.createTimeRanges 26315 */ 26316 videojs$1.createTimeRange = videojs$1.createTimeRanges = createTimeRanges; 26317 26318 /** 26319 * Format seconds as a time string, H:MM:SS or M:SS 26320 * Supplying a guide (in seconds) will force a number of leading zeros 26321 * to cover the length of the guide 26322 * 26323 * @borrows format-time:formatTime as videojs.formatTime 26324 */ 26325 videojs$1.formatTime = formatTime; 26326 26327 /** 26328 * Replaces format-time with a custom implementation, to be used in place of the default. 26329 * 26330 * @borrows format-time:setFormatTime as videojs.setFormatTime 26331 * 26332 * @method setFormatTime 26333 * 26334 * @param {Function} customFn 26335 * A custom format-time function which will be called with the current time and guide (in seconds) as arguments. 26336 * Passed fn should return a string. 26337 */ 26338 videojs$1.setFormatTime = setFormatTime; 26339 26340 /** 26341 * Resets format-time to the default implementation. 26342 * 26343 * @borrows format-time:resetFormatTime as videojs.resetFormatTime 26344 * 26345 * @method resetFormatTime 26346 */ 26347 videojs$1.resetFormatTime = resetFormatTime; 26348 26349 /** 26350 * Resolve and parse the elements of a URL 26351 * 26352 * @borrows url:parseUrl as videojs.parseUrl 26353 * 26354 */ 26355 videojs$1.parseUrl = parseUrl; 26356 26357 /** 26358 * Returns whether the url passed is a cross domain request or not. 26359 * 26360 * @borrows url:isCrossOrigin as videojs.isCrossOrigin 26361 */ 26362 videojs$1.isCrossOrigin = isCrossOrigin; 26363 26364 /** 26365 * Event target class. 26366 * 26367 * @borrows EventTarget as videojs.EventTarget 26368 */ 26369 videojs$1.EventTarget = EventTarget; 26370 26371 /** 26372 * Add an event listener to element 26373 * It stores the handler function in a separate cache object 26374 * and adds a generic handler to the element's event, 26375 * along with a unique id (guid) to the element. 26376 * 26377 * @borrows events:on as videojs.on 26378 */ 26379 videojs$1.on = on; 26380 26381 /** 26382 * Trigger a listener only once for an event 26383 * 26384 * @borrows events:one as videojs.one 26385 */ 26386 videojs$1.one = one; 26387 26388 /** 26389 * Removes event listeners from an element 26390 * 26391 * @borrows events:off as videojs.off 26392 */ 26393 videojs$1.off = off; 26394 26395 /** 26396 * Trigger an event for an element 26397 * 26398 * @borrows events:trigger as videojs.trigger 26399 */ 26400 videojs$1.trigger = trigger; 26401 26402 /** 26403 * A cross-browser XMLHttpRequest wrapper. Here's a simple example: 26404 * 26405 * @param {Object} options 26406 * settings for the request. 26407 * 26408 * @return {XMLHttpRequest|XDomainRequest} 26409 * The request object. 26410 * 26411 * @see https://github.com/Raynos/xhr 26412 */ 26413 videojs$1.xhr = xhr; 26414 26415 /** 26416 * TextTrack class 26417 * 26418 * @borrows TextTrack as videojs.TextTrack 26419 */ 26420 videojs$1.TextTrack = TextTrack; 26421 26422 /** 26423 * export the AudioTrack class so that source handlers can create 26424 * AudioTracks and then add them to the players AudioTrackList 26425 * 26426 * @borrows AudioTrack as videojs.AudioTrack 26427 */ 26428 videojs$1.AudioTrack = AudioTrack; 26429 26430 /** 26431 * export the VideoTrack class so that source handlers can create 26432 * VideoTracks and then add them to the players VideoTrackList 26433 * 26434 * @borrows VideoTrack as videojs.VideoTrack 26435 */ 26436 videojs$1.VideoTrack = VideoTrack; 26437 26438 /** 26439 * Determines, via duck typing, whether or not a value is a DOM element. 26440 * 26441 * @borrows dom:isEl as videojs.isEl 26442 * @deprecated Use videojs.dom.isEl() instead 26443 */ 26444 26445 /** 26446 * Determines, via duck typing, whether or not a value is a text node. 26447 * 26448 * @borrows dom:isTextNode as videojs.isTextNode 26449 * @deprecated Use videojs.dom.isTextNode() instead 26450 */ 26451 26452 /** 26453 * Creates an element and applies properties. 26454 * 26455 * @borrows dom:createEl as videojs.createEl 26456 * @deprecated Use videojs.dom.createEl() instead 26457 */ 26458 26459 /** 26460 * Check if an element has a CSS class 26461 * 26462 * @borrows dom:hasElClass as videojs.hasClass 26463 * @deprecated Use videojs.dom.hasClass() instead 26464 */ 26465 26466 /** 26467 * Add a CSS class name to an element 26468 * 26469 * @borrows dom:addElClass as videojs.addClass 26470 * @deprecated Use videojs.dom.addClass() instead 26471 */ 26472 26473 /** 26474 * Remove a CSS class name from an element 26475 * 26476 * @borrows dom:removeElClass as videojs.removeClass 26477 * @deprecated Use videojs.dom.removeClass() instead 26478 */ 26479 26480 /** 26481 * Adds or removes a CSS class name on an element depending on an optional 26482 * condition or the presence/absence of the class name. 26483 * 26484 * @borrows dom:toggleElClass as videojs.toggleClass 26485 * @deprecated Use videojs.dom.toggleClass() instead 26486 */ 26487 26488 /** 26489 * Apply attributes to an HTML element. 26490 * 26491 * @borrows dom:setElAttributes as videojs.setAttribute 26492 * @deprecated Use videojs.dom.setAttributes() instead 26493 */ 26494 26495 /** 26496 * Get an element's attribute values, as defined on the HTML tag 26497 * Attributes are not the same as properties. They're defined on the tag 26498 * or with setAttribute (which shouldn't be used with HTML) 26499 * This will return true or false for boolean attributes. 26500 * 26501 * @borrows dom:getElAttributes as videojs.getAttributes 26502 * @deprecated Use videojs.dom.getAttributes() instead 26503 */ 26504 26505 /** 26506 * Empties the contents of an element. 26507 * 26508 * @borrows dom:emptyEl as videojs.emptyEl 26509 * @deprecated Use videojs.dom.emptyEl() instead 26510 */ 26511 26512 /** 26513 * Normalizes and appends content to an element. 26514 * 26515 * The content for an element can be passed in multiple types and 26516 * combinations, whose behavior is as follows: 26517 * 26518 * - String 26519 * Normalized into a text node. 26520 * 26521 * - Element, TextNode 26522 * Passed through. 26523 * 26524 * - Array
26525 * A one-dimensional array of strings, elements, nodes, or functions (which 26526 * return single strings, elements, or nodes). 26527 * 26528 * - Function 26529 * If the sole argument, is expected to produce a string, element, 26530 * node, or array. 26531 * 26532 * @borrows dom:appendContents as videojs.appendContet 26533 * @deprecated Use videojs.dom.appendContent() instead 26534 */ 26535 26536 /** 26537 * Normalizes and inserts content into an element; this is identical to 26538 * `appendContent()`, except it empties the element first. 26539 * 26540 * The content for an element can be passed in multiple types and 26541 * combinations, whose behavior is as follows: 26542 * 26543 * - String 26544 * Normalized into a text node. 26545 * 26546 * - Element, TextNode 26547 * Passed through. 26548 * 26549 * - Array 26550 * A one-dimensional array of strings, elements, nodes, or functions (which 26551 * return single strings, elements, or nodes). 26552 * 26553 * - Function 26554 * If the sole argument, is expected to produce a string, element, 26555 * node, or array. 26556 * 26557 * @borrows dom:insertContent as videojs.insertContent 26558 * @deprecated Use videojs.dom.insertContent() instead 26559 */ 26560 ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(function (k) { 26561 videojs$1[k] = function () { 26562 log$1.warn('videojs.' + k + '() is deprecated; use videojs.dom.' + k + '() instead'); 26563 return Dom[k].apply(null, arguments); 26564 }; 26565 }); 26566 26567 /** 26568 * A safe getComputedStyle. 26569 * 26570 * This is because in Firefox, if the player is loaded in an iframe with `display:none`, 26571 * then `getComputedStyle` returns `null`, so, we do a null-check to make sure 26572 * that the player doesn't break in these cases. 26573 * See https://bugzilla.mozilla.org/show_bug.cgi?id=548397 for more details. 26574 * 26575 * @borrows computed-style:computedStyle as videojs.computedStyle 26576 */ 26577 videojs$1.computedStyle = computedStyle; 26578 26579 /** 26580 * Export the Dom utilities for use in external plugins 26581 * and Tech's 26582 */ 26583 videojs$1.dom = Dom; 26584 26585 /** 26586 * Export the Url utilities for use in external plugins 26587 * and Tech's 26588 */ 26589 videojs$1.url = Url; 26590 26591 var urlToolkit = createCommonjsModule(function (module, exports) { 26592 // see https://tools.ietf.org/html/rfc1808 26593 26594 /* jshint ignore:start */ 26595 (function (root) { 26596 /* jshint ignore:end */ 26597 26598 var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/\;?#]*)?(.*?)??(;.*?)?(\?.*?)?(#.*?)?$/; 26599 var FIRST_SEGMENT_REGEX = /^([^\/;?#]*)(.*)$/; 26600 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 26601 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g; 26602 26603 var URLToolkit = { // jshint ignore:line 26604 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 26605 // E.g 26606 // With opts.alwaysNormalize = false (default, spec compliant) 26607 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 26608 // With opts.alwaysNormalize = true (not spec compliant) 26609 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 26610 buildAbsoluteURL: function buildAbsoluteURL(baseURL, relativeURL, opts) { 26611 opts = opts || {}; 26612 // remove any remaining space and CRLF 26613 baseURL = baseURL.trim(); 26614 relativeURL = relativeURL.trim(); 26615 if (!relativeURL) { 26616 // 2a) If the embedded URL is entirely empty, it inherits the 26617 // entire base URL (i.e., is set equal to the base URL) 26618 // and we are done. 26619 if (!opts.alwaysNormalize) { 26620 return baseURL; 26621 } 26622 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 26623 if (!basePartsForNormalise) { 26624 throw new Error('Error trying to parse base URL.'); 26625 } 26626 basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); 26627 return URLToolkit.buildURLFromParts(basePartsForNormalise); 26628 } 26629 var relativeParts = URLToolkit.parseURL(relativeURL); 26630 if (!relativeParts) { 26631 throw new Error('Error trying to parse relative URL.'); 26632 } 26633 if (relativeParts.scheme) { 26634 // 2b) If the embedded URL starts with a scheme name, it is 26635 // interpreted as an absolute URL and we are done.
vendor: 2,564 bytes, lines 26636-26685
26636 if (!opts.alwaysNormalize) { 26637 return relativeURL; 26638 } 26639 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 26640 return URLToolkit.buildURLFromParts(relativeParts); 26641 } 26642 var baseParts = URLToolkit.parseURL(baseURL); 26643 if (!baseParts) { 26644 throw new Error('Error trying to parse base URL.'); 26645 } 26646 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 26647 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 26648 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 26649 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 26650 baseParts.netLoc = pathParts[1]; 26651 baseParts.path = pathParts[2]; 26652 } 26653 if (baseParts.netLoc && !baseParts.path) { 26654 baseParts.path = '/'; 26655 } 26656 var builtParts = { 26657 // 2c) Otherwise, the embedded URL inherits the scheme of 26658 // the base URL. 26659 scheme: baseParts.scheme, 26660 netLoc: relativeParts.netLoc, 26661 path: null, 26662 params: relativeParts.params, 26663 query: relativeParts.query, 26664 fragment: relativeParts.fragment 26665 }; 26666 if (!relativeParts.netLoc) { 26667 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 26668 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 26669 // (if any) of the base URL. 26670 builtParts.netLoc = baseParts.netLoc; 26671 // 4) If the embedded URL path is preceded by a slash "/", the 26672 // path is not relative and we skip to Step 7. 26673 if (relativeParts.path[0] !== '/') { 26674 if (!relativeParts.path) { 26675 // 5) If the embedded URL path is empty (and not preceded by a 26676 // slash), then the embedded URL inherits the base URL path 26677 builtParts.path = baseParts.path; 26678 // 5a) if the embedded URL's <params> is non-empty, we skip to 26679 // step 7; otherwise, it inherits the <params> of the base 26680 // URL (if any) and 26681 if (!relativeParts.params) { 26682 builtParts.params = baseParts.params; 26683 // 5b) if the embedded URL's <query> is non-empty, we skip to 26684 // step 7; otherwise, it inherits the <query> of the base 26685 // URL (if any) and we skip to step 7.
vendor: 63,872 bytes, lines 26686-28606
26686 if (!relativeParts.query) { 26687 builtParts.query = baseParts.query; 26688 } 26689 } 26690 } else { 26691 // 6) The last segment of the base URL's path (anything 26692 // following the rightmost slash "/", or the entire path if no 26693 // slash is present) is removed and the embedded URL's path is 26694 // appended in its place. 26695 var baseURLPath = baseParts.path; 26696 var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; 26697 builtParts.path = URLToolkit.normalizePath(newPath); 26698 } 26699 } 26700 } 26701 if (builtParts.path === null) { 26702 builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; 26703 } 26704 return URLToolkit.buildURLFromParts(builtParts); 26705 }, 26706 parseURL: function parseURL(url) { 26707 var parts = URL_REGEX.exec(url); 26708 if (!parts) { 26709 return null; 26710 } 26711 return { 26712 scheme: parts[1] || '', 26713 netLoc: parts[2] || '', 26714 path: parts[3] || '', 26715 params: parts[4] || '', 26716 query: parts[5] || '', 26717 fragment: parts[6] || '' 26718 }; 26719 }, 26720 normalizePath: function normalizePath(path) { 26721 // The following operations are 26722 // then applied, in order, to the new path: 26723 // 6a) All occurrences of "./", where "." is a complete path 26724 // segment, are removed. 26725 // 6b) If the path ends with "." as a complete path segment, 26726 // that "." is removed. 26727 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); 26728 // 6c) All occurrences of "<segment>/../", where <segment> is a 26729 // complete path segment not equal to "..", are removed. 26730 // Removal of these path segments is performed iteratively, 26731 // removing the leftmost matching pattern on each iteration, 26732 // until no matching pattern remains. 26733 // 6d) If the path ends with "<segment>/..", where <segment> is a 26734 // complete path segment not equal to "..", that 26735 // "<segment>/.." is removed. 26736 while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} // jshint ignore:line 26737 return path.split('').reverse().join(''); 26738 }, 26739 buildURLFromParts: function buildURLFromParts(parts) { 26740 return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; 26741 } 26742 }; 26743 26744 /* jshint ignore:start */ 26745 module.exports = URLToolkit; 26746 })(commonjsGlobal); 26747 /* jshint ignore:end */ 26748 }); 26749 26750 var classCallCheck$1 = function classCallCheck$$1(instance, Constructor) { 26751 if (!(instance instanceof Constructor)) { 26752 throw new TypeError("Cannot call a class as a function"); 26753 } 26754 }; 26755 26756 var _extends$1 = Object.assign || function (target) { 26757 for (var i = 1; i < arguments.length; i++) { 26758 var source = arguments[i]; 26759 26760 for (var key in source) { 26761 if (Object.prototype.hasOwnProperty.call(source, key)) { 26762 target[key] = source[key]; 26763 } 26764 } 26765 } 26766 26767 return target; 26768 }; 26769 26770 var inherits$1 = function inherits$$1(subClass, superClass) { 26771 if (typeof superClass !== "function" && superClass !== null) { 26772 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === "undefined" ? "undefined" : _typeof(superClass))); 26773 } 26774 26775 subClass.prototype = Object.create(superClass && superClass.prototype, { 26776 constructor: { 26777 value: subClass, 26778 enumerable: false, 26779 writable: true, 26780 configurable: true 26781 } 26782 }); 26783 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 26784 }; 26785 26786 var possibleConstructorReturn$1 = function possibleConstructorReturn$$1(self, call) { 26787 if (!self) { 26788 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 26789 } 26790 26791 return call && ((typeof call === "undefined" ? "undefined" : _typeof(call)) === "object" || typeof call === "function") ? call : self; 26792 }; 26793 26794 /** 26795 * @file stream.js 26796 */ 26797 /** 26798 * A lightweight readable stream implemention that handles event dispatching. 26799 * 26800 * @class Stream 26801 */ 26802 var Stream = function () { 26803 function Stream() { 26804 classCallCheck$1(this, Stream); 26805 26806 this.listeners = {}; 26807 } 26808 26809 /** 26810 * Add a listener for a specified event type. 26811 * 26812 * @param {String} type the event name 26813 * @param {Function} listener the callback to be invoked when an event of 26814 * the specified type occurs 26815 */ 26816 26817 Stream.prototype.on = function on(type, listener) { 26818 if (!this.listeners[type]) { 26819 this.listeners[type] = []; 26820 } 26821 this.listeners[type].push(listener); 26822 }; 26823 26824 /** 26825 * Remove a listener for a specified event type. 26826 * 26827 * @param {String} type the event name 26828 * @param {Function} listener a function previously registered for this 26829 * type of event through `on` 26830 * @return {Boolean} if we could turn it off or not 26831 */ 26832 26833 Stream.prototype.off = function off(type, listener) { 26834 if (!this.listeners[type]) { 26835 return false; 26836 } 26837 26838 var index = this.listeners[type].indexOf(listener); 26839 26840 this.listeners[type].splice(index, 1); 26841 return index > -1; 26842 }; 26843 26844 /** 26845 * Trigger an event of the specified type on this stream. Any additional 26846 * arguments to this function are passed as parameters to event listeners. 26847 * 26848 * @param {String} type the event name 26849 */ 26850 26851 Stream.prototype.trigger = function trigger(type) { 26852 var callbacks = this.listeners[type]; 26853 var i = void 0; 26854 var length = void 0; 26855 var args = void 0; 26856 26857 if (!callbacks) { 26858 return; 26859 } 26860 // Slicing the arguments on every invocation of this method 26861 // can add a significant amount of overhead. Avoid the 26862 // intermediate object creation for the common case of a 26863 // single callback argument 26864 if (arguments.length === 2) { 26865 length = callbacks.length; 26866 for (i = 0; i < length; ++i) { 26867 callbacks[i].call(this, arguments[1]); 26868 } 26869 } else { 26870 args = Array.prototype.slice.call(arguments, 1); 26871 length = callbacks.length; 26872 for (i = 0; i < length; ++i) { 26873 callbacks[i].apply(this, args); 26874 } 26875 } 26876 }; 26877 26878 /** 26879 * Destroys the stream and cleans up. 26880 */ 26881 26882 Stream.prototype.dispose = function dispose() { 26883 this.listeners = {}; 26884 }; 26885 /** 26886 * Forwards all `data` events on this stream to the destination stream. The 26887 * destination stream should provide a method `push` to receive the data 26888 * events as they arrive. 26889 * 26890 * @param {Stream} destination the stream that will receive all `data` events 26891 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 26892 */ 26893 26894 Stream.prototype.pipe = function pipe(destination) { 26895 this.on('data', function (data) { 26896 destination.push(data); 26897 }); 26898 }; 26899 26900 return Stream; 26901 }(); 26902 26903 /** 26904 * @file m3u8/line-stream.js 26905 */ 26906 /** 26907 * A stream that buffers string input and generates a `data` event for each 26908 * line. 26909 * 26910 * @class LineStream 26911 * @extends Stream 26912 */ 26913 26914 var LineStream = function (_Stream) { 26915 inherits$1(LineStream, _Stream); 26916 26917 function LineStream() { 26918 classCallCheck$1(this, LineStream); 26919 26920 var _this = possibleConstructorReturn$1(this, _Stream.call(this)); 26921 26922 _this.buffer = ''; 26923 return _this; 26924 } 26925 26926 /** 26927 * Add new data to be parsed. 26928 * 26929 * @param {String} data the text to process 26930 */ 26931 26932 LineStream.prototype.push = function push(data) { 26933 var nextNewline = void 0; 26934 26935 this.buffer += data; 26936 nextNewline = this.buffer.indexOf('\n'); 26937 26938 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { 26939 this.trigger('data', this.buffer.substring(0, nextNewline)); 26940 this.buffer = this.buffer.substring(nextNewline + 1); 26941 } 26942 }; 26943 26944 return LineStream; 26945 }(Stream); 26946 26947 /** 26948 * @file m3u8/parse-stream.js 26949 */ 26950 /** 26951 * "forgiving" attribute list psuedo-grammar: 26952 * attributes -> keyvalue (',' keyvalue)* 26953 * keyvalue -> key '=' value 26954 * key -> [^=]* 26955 * value -> '"' [^"]* '"' | [^,]* 26956 */ 26957 var attributeSeparator = function attributeSeparator() { 26958 var key = '[^=]*'; 26959 var value = '"[^"]*"|[^,]*'; 26960 var keyvalue = '(?:' + key + ')=(?:' + value + ')'; 26961 26962 return new RegExp('(?:^|,)(' + keyvalue + ')'); 26963 }; 26964 26965 /** 26966 * Parse attributes from a line given the seperator 26967 * 26968 * @param {String} attributes the attibute line to parse 26969 */ 26970 var parseAttributes = function parseAttributes(attributes) { 26971 // split the string using attributes as the separator 26972 var attrs = attributes.split(attributeSeparator()); 26973 var result = {}; 26974 var i = attrs.length; 26975 var attr = void 0; 26976 26977 while (i--) { 26978 // filter out unmatched portions of the string 26979 if (attrs[i] === '') { 26980 continue; 26981 } 26982 26983 // split the key and value 26984 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); 26985 // trim whitespace and remove optional quotes around the value 26986 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 26987 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 26988 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 26989 result[attr[0]] = attr[1]; 26990 } 26991 return result; 26992 }; 26993 26994 /** 26995 * A line-level M3U8 parser event stream. It expects to receive input one 26996 * line at a time and performs a context-free parse of its contents. A stream 26997 * interpretation of a manifest can be useful if the manifest is expected to 26998 * be too large to fit comfortably into memory or the entirety of the input 26999 * is not immediately available. Otherwise, it's probably much easier to work 27000 * with a regular `Parser` object. 27001 * 27002 * Produces `data` events with an object that captures the parser's 27003 * interpretation of the input. That object has a property `tag` that is one 27004 * of `uri`, `comment`, or `tag`. URIs only have a single additional 27005 * property, `line`, which captures the entirety of the input without 27006 * interpretation. Comments similarly have a single additional property 27007 * `text` which is the input without the leading `#`. 27008 * 27009 * Tags always have a property `tagType` which is the lower-cased version of 27010 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, 27011 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 27012 * tags are given the tag type `unknown` and a single additional property 27013 * `data` with the remainder of the input. 27014 * 27015 * @class ParseStream 27016 * @extends Stream 27017 */ 27018 27019 var ParseStream = function (_Stream) { 27020 inherits$1(ParseStream, _Stream); 27021 27022 function ParseStream() { 27023 classCallCheck$1(this, ParseStream); 27024 27025 var _this = possibleConstructorReturn$1(this, _Stream.call(this)); 27026 27027 _this.customParsers = []; 27028 return _this; 27029 } 27030 27031 /** 27032 * Parses an additional line of input. 27033 * 27034 * @param {String} line a single line of an M3U8 file to parse 27035 */ 27036 27037 ParseStream.prototype.push = function push(line) { 27038 var match = void 0; 27039 var event = void 0; 27040 27041 // strip whitespace 27042 line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, ''); 27043 if (line.length === 0) { 27044 // ignore empty lines 27045 return; 27046 } 27047 27048 // URIs 27049 if (line[0] !== '#') { 27050 this.trigger('data', { 27051 type: 'uri', 27052 uri: line 27053 }); 27054 return; 27055 } 27056 27057 for (var i = 0; i < this.customParsers.length; i++) { 27058 if (this.customParsers[i].call(this, line)) { 27059 return; 27060 } 27061 } 27062 27063 // Comments 27064 if (line.indexOf('#EXT') !== 0) { 27065 this.trigger('data', { 27066 type: 'comment', 27067 text: line.slice(1) 27068 }); 27069 return; 27070 } 27071 27072 // strip off any carriage returns here so the regex matching 27073 // doesn't have to account for them. 27074 line = line.replace('\r', ''); 27075 27076 // Tags 27077 match = /^#EXTM3U/.exec(line); 27078 if (match) { 27079 this.trigger('data', { 27080 type: 'tag', 27081 tagType: 'm3u' 27082 }); 27083 return; 27084 } 27085 match = /^#EXTINF:?([0-9\.]*)?,?(.*)?$/.exec(line); 27086 if (match) { 27087 event = { 27088 type: 'tag', 27089 tagType: 'inf' 27090 }; 27091 if (match[1]) { 27092 event.duration = parseFloat(match[1]); 27093 } 27094 if (match[2]) { 27095 event.title = match[2]; 27096 } 27097 this.trigger('data', event); 27098 return; 27099 } 27100 match = /^#EXT-X-TARGETDURATION:?([0-9.]*)?/.exec(line); 27101 if (match) { 27102 event = { 27103 type: 'tag', 27104 tagType: 'targetduration' 27105 }; 27106 if (match[1]) { 27107 event.duration = parseInt(match[1], 10); 27108 } 27109 this.trigger('data', event); 27110 return; 27111 } 27112 match = /^#ZEN-TOTAL-DURATION:?([0-9.]*)?/.exec(line); 27113 if (match) { 27114 event = { 27115 type: 'tag', 27116 tagType: 'totalduration' 27117 }; 27118 if (match[1]) { 27119 event.duration = parseInt(match[1], 10); 27120 } 27121 this.trigger('data', event); 27122 return; 27123 } 27124 match = /^#EXT-X-VERSION:?([0-9.]*)?/.exec(line); 27125 if (match) { 27126 event = { 27127 type: 'tag', 27128 tagType: 'version' 27129 }; 27130 if (match[1]) { 27131 event.version = parseInt(match[1], 10); 27132 } 27133 this.trigger('data', event); 27134 return; 27135 } 27136 match = /^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/.exec(line); 27137 if (match) { 27138 event = { 27139 type: 'tag', 27140 tagType: 'media-sequence' 27141 }; 27142 if (match[1]) { 27143 event.number = parseInt(match[1], 10); 27144 } 27145 this.trigger('data', event); 27146 return; 27147 } 27148 match = /^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/.exec(line); 27149 if (match) { 27150 event = { 27151 type: 'tag', 27152 tagType: 'discontinuity-sequence' 27153 }; 27154 if (match[1]) { 27155 event.number = parseInt(match[1], 10); 27156 } 27157 this.trigger('data', event); 27158 return; 27159 } 27160 match = /^#EXT-X-PLAYLIST-TYPE:?(.*)?$/.exec(line); 27161 if (match) { 27162 event = { 27163 type: 'tag', 27164 tagType: 'playlist-type' 27165 }; 27166 if (match[1]) { 27167 event.playlistType = match[1]; 27168 } 27169 this.trigger('data', event); 27170 return; 27171 } 27172 match = /^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/.exec(line); 27173 if (match) { 27174 event = { 27175 type: 'tag', 27176 tagType: 'byterange' 27177 }; 27178 if (match[1]) { 27179 event.length = parseInt(match[1], 10); 27180 } 27181 if (match[2]) { 27182 event.offset = parseInt(match[2], 10); 27183 } 27184 this.trigger('data', event); 27185 return; 27186 } 27187 match = /^#EXT-X-ALLOW-CACHE:?(YES|NO)?/.exec(line); 27188 if (match) { 27189 event = { 27190 type: 'tag', 27191 tagType: 'allow-cache' 27192 }; 27193 if (match[1]) { 27194 event.allowed = !/NO/.test(match[1]); 27195 } 27196 this.trigger('data', event); 27197 return; 27198 } 27199 match = /^#EXT-X-MAP:?(.*)$/.exec(line); 27200 if (match) { 27201 event = { 27202 type: 'tag', 27203 tagType: 'map' 27204 }; 27205 27206 if (match[1]) { 27207 var attributes = parseAttributes(match[1]); 27208 27209 if (attributes.URI) { 27210 event.uri = attributes.URI; 27211 } 27212 if (attributes.BYTERANGE) { 27213 var _attributes$BYTERANGE = attributes.BYTERANGE.split('@'), 27214 length = _attributes$BYTERANGE[0], 27215 offset = _attributes$BYTERANGE[1]; 27216 27217 event.byterange = {}; 27218 if (length) { 27219 event.byterange.length = parseInt(length, 10); 27220 } 27221 if (offset) { 27222 event.byterange.offset = parseInt(offset, 10); 27223 } 27224 } 27225 } 27226 27227 this.trigger('data', event); 27228 return; 27229 } 27230 match = /^#EXT-X-STREAM-INF:?(.*)$/.exec(line); 27231 if (match) { 27232 event = { 27233 type: 'tag', 27234 tagType: 'stream-inf' 27235 }; 27236 if (match[1]) { 27237 event.attributes = parseAttributes(match[1]); 27238 27239 if (event.attributes.RESOLUTION) { 27240 var split = event.attributes.RESOLUTION.split('x'); 27241 var resolution = {}; 27242 27243 if (split[0]) { 27244 resolution.width = parseInt(split[0], 10); 27245 } 27246 if (split[1]) { 27247 resolution.height = parseInt(split[1], 10); 27248 } 27249 event.attributes.RESOLUTION = resolution; 27250 } 27251 if (event.attributes.BANDWIDTH) { 27252 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 27253 } 27254 if (event.attributes['PROGRAM-ID']) { 27255 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); 27256 } 27257 } 27258 this.trigger('data', event); 27259 return; 27260 } 27261 match = /^#EXT-X-MEDIA:?(.*)$/.exec(line); 27262 if (match) { 27263 event = { 27264 type: 'tag', 27265 tagType: 'media' 27266 }; 27267 if (match[1]) { 27268 event.attributes = parseAttributes(match[1]); 27269 } 27270 this.trigger('data', event); 27271 return; 27272 } 27273 match = /^#EXT-X-ENDLIST/.exec(line); 27274 if (match) { 27275 this.trigger('data', { 27276 type: 'tag', 27277 tagType: 'endlist' 27278 }); 27279 return; 27280 } 27281 match = /^#EXT-X-DISCONTINUITY/.exec(line); 27282 if (match) { 27283 this.trigger('data', { 27284 type: 'tag', 27285 tagType: 'discontinuity' 27286 }); 27287 return; 27288 } 27289 match = /^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/.exec(line); 27290 if (match) { 27291 event = { 27292 type: 'tag', 27293 tagType: 'program-date-time' 27294 }; 27295 if (match[1]) { 27296 event.dateTimeString = match[1]; 27297 event.dateTimeObject = new Date(match[1]); 27298 } 27299 this.trigger('data', event); 27300 return; 27301 } 27302 match = /^#EXT-X-KEY:?(.*)$/.exec(line); 27303 if (match) { 27304 event = { 27305 type: 'tag', 27306 tagType: 'key' 27307 }; 27308 if (match[1]) { 27309 event.attributes = parseAttributes(match[1]); 27310 // parse the IV string into a Uint32Array 27311 if (event.attributes.IV) { 27312 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { 27313 event.attributes.IV = event.attributes.IV.substring(2); 27314 } 27315 27316 event.attributes.IV = event.attributes.IV.match(/.{8}/g); 27317 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); 27318 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); 27319 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); 27320 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); 27321 event.attributes.IV = new Uint32Array(event.attributes.IV); 27322 } 27323 } 27324 this.trigger('data', event); 27325 return; 27326 } 27327 match = /^#EXT-X-START:?(.*)$/.exec(line); 27328 if (match) { 27329 event = { 27330 type: 'tag', 27331 tagType: 'start' 27332 }; 27333 if (match[1]) { 27334 event.attributes = parseAttributes(match[1]); 27335 27336 event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); 27337 event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); 27338 } 27339 this.trigger('data', event); 27340 return; 27341 } 27342 match = /^#EXT-X-CUE-OUT-CONT:?(.*)?$/.exec(line); 27343 if (match) { 27344 event = { 27345 type: 'tag', 27346 tagType: 'cue-out-cont' 27347 }; 27348 if (match[1]) { 27349 event.data = match[1]; 27350 } else { 27351 event.data = ''; 27352 } 27353 this.trigger('data', event); 27354 return; 27355 } 27356 match = /^#EXT-X-CUE-OUT:?(.*)?$/.exec(line); 27357 if (match) { 27358 event = { 27359 type: 'tag', 27360 tagType: 'cue-out' 27361 }; 27362 if (match[1]) { 27363 event.data = match[1]; 27364 } else { 27365 event.data = ''; 27366 } 27367 this.trigger('data', event); 27368 return; 27369 } 27370 match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(line); 27371 if (match) { 27372 event = { 27373 type: 'tag', 27374 tagType: 'cue-in' 27375 }; 27376 if (match[1]) { 27377 event.data = match[1]; 27378 } else { 27379 event.data = ''; 27380 } 27381 this.trigger('data', event); 27382 return; 27383 } 27384 27385 // unknown tag type 27386 this.trigger('data', { 27387 type: 'tag', 27388 data: line.slice(4) 27389 }); 27390 }; 27391 27392 /** 27393 * Add a parser for custom headers 27394 * 27395 * @param {Object} options a map of options for the added parser 27396 * @param {RegExp} options.expression a regular expression to match the custom header 27397 * @param {string} options.customType the custom type to register to the output 27398 * @param {Function} [options.dataParser] function to parse the line into an object 27399 * @param {boolean} [options.segment] should tag data be attached to the segment object 27400 */ 27401 27402 ParseStream.prototype.addParser = function addParser(_ref) { 27403 var _this2 = this; 27404 27405 var expression = _ref.expression, 27406 customType = _ref.customType, 27407 dataParser = _ref.dataParser, 27408 segment = _ref.segment; 27409 27410 if (typeof dataParser !== 'function') { 27411 dataParser = function dataParser(line) { 27412 return line; 27413 }; 27414 } 27415 this.customParsers.push(function (line) { 27416 var match = expression.exec(line); 27417 27418 if (match) { 27419 _this2.trigger('data', { 27420 type: 'custom', 27421 data: dataParser(line), 27422 customType: customType, 27423 segment: segment 27424 }); 27425 return true; 27426 } 27427 }); 27428 }; 27429 27430 return ParseStream; 27431 }(Stream); 27432 27433 /** 27434 * @file m3u8/parser.js 27435 */ 27436 /** 27437 * A parser for M3U8 files. The current interpretation of the input is 27438 * exposed as a property `manifest` on parser objects. It's just two lines to 27439 * create and parse a manifest once you have the contents available as a string: 27440 * 27441 * ```js 27442 * var parser = new m3u8.Parser(); 27443 * parser.push(xhr.responseText); 27444 * ``` 27445 * 27446 * New input can later be applied to update the manifest object by calling 27447 * `push` again. 27448 * 27449 * The parser attempts to create a usable manifest object even if the 27450 * underlying input is somewhat nonsensical. It emits `info` and `warning` 27451 * events during the parse if it encounters input that seems invalid or 27452 * requires some property of the manifest object to be defaulted. 27453 * 27454 * @class Parser 27455 * @extends Stream 27456 */ 27457 27458 var Parser = function (_Stream) { 27459 inherits$1(Parser, _Stream); 27460 27461 function Parser() { 27462 classCallCheck$1(this, Parser); 27463 27464 var _this = possibleConstructorReturn$1(this, _Stream.call(this)); 27465 27466 _this.lineStream = new LineStream(); 27467 _this.parseStream = new ParseStream(); 27468 _this.lineStream.pipe(_this.parseStream); 27469 27470 /* eslint-disable consistent-this */ 27471 var self = _this; 27472 /* eslint-enable consistent-this */ 27473 var uris = []; 27474 var currentUri = {}; 27475 // if specified, the active EXT-X-MAP definition 27476 var currentMap = void 0; 27477 // if specified, the active decryption key 27478 var _key = void 0; 27479 var noop = function noop() {}; 27480 var defaultMediaGroups = { 27481 'AUDIO': {}, 27482 'VIDEO': {}, 27483 'CLOSED-CAPTIONS': {}, 27484 'SUBTITLES': {} 27485 }; 27486 // group segments into numbered timelines delineated by discontinuities 27487 var currentTimeline = 0; 27488 27489 // the manifest is empty until the parse stream begins delivering data 27490 _this.manifest = { 27491 allowCache: true, 27492 discontinuityStarts: [], 27493 segments: [] 27494 }; 27495 27496 // update the manifest with the m3u8 entry from the parse stream 27497 _this.parseStream.on('data', function (entry) { 27498 var mediaGroup = void 0; 27499 var rendition = void 0; 27500 27501 ({ 27502 tag: function tag() { 27503 // switch based on the tag type 27504 (({ 27505 'allow-cache': function allowCache() { 27506 this.manifest.allowCache = entry.allowed; 27507 if (!('allowed' in entry)) { 27508 this.trigger('info', { 27509 message: 'defaulting allowCache to YES' 27510 }); 27511 this.manifest.allowCache = true; 27512 } 27513 }, 27514 byterange: function byterange() { 27515 var byterange = {}; 27516 27517 if ('length' in entry) { 27518 currentUri.byterange = byterange; 27519 byterange.length = entry.length; 27520 27521 if (!('offset' in entry)) { 27522 this.trigger('info', { 27523 message: 'defaulting offset to zero' 27524 }); 27525 entry.offset = 0; 27526 } 27527 } 27528 if ('offset' in entry) { 27529 currentUri.byterange = byterange; 27530 byterange.offset = entry.offset; 27531 } 27532 }, 27533 endlist: function endlist() { 27534 this.manifest.endList = true; 27535 }, 27536 inf: function inf() { 27537 if (!('mediaSequence' in this.manifest)) { 27538 this.manifest.mediaSequence = 0; 27539 this.trigger('info', { 27540 message: 'defaulting media sequence to zero' 27541 }); 27542 } 27543 if (!('discontinuitySequence' in this.manifest)) { 27544 this.manifest.discontinuitySequence = 0; 27545 this.trigger('info', { 27546 message: 'defaulting discontinuity sequence to zero' 27547 }); 27548 } 27549 if (entry.duration > 0) { 27550 currentUri.duration = entry.duration; 27551 } 27552 27553 if (entry.duration === 0) { 27554 currentUri.duration = 0.01; 27555 this.trigger('info', { 27556 message: 'updating zero segment duration to a small value' 27557 }); 27558 } 27559 27560 this.manifest.segments = uris; 27561 }, 27562 key: function key() { 27563 if (!entry.attributes) { 27564 this.trigger('warn', { 27565 message: 'ignoring key declaration without attribute list' 27566 }); 27567 return; 27568 } 27569 // clear the active encryption key 27570 if (entry.attributes.METHOD === 'NONE') { 27571 _key = null; 27572 return; 27573 } 27574 if (!entry.attributes.URI) { 27575 this.trigger('warn', { 27576 message: 'ignoring key declaration without URI' 27577 }); 27578 return; 27579 } 27580 if (!entry.attributes.METHOD) { 27581 this.trigger('warn', { 27582 message: 'defaulting key method to AES-128' 27583 }); 27584 } 27585 27586 // setup an encryption key for upcoming segments 27587 _key = { 27588 method: entry.attributes.METHOD || 'AES-128', 27589 uri: entry.attributes.URI 27590 }; 27591 27592 if (typeof entry.attributes.IV !== 'undefined') { 27593 _key.iv = entry.attributes.IV; 27594 } 27595 }, 27596 'media-sequence': function mediaSequence() { 27597 if (!isFinite(entry.number)) { 27598 this.trigger('warn', { 27599 message: 'ignoring invalid media sequence: ' + entry.number 27600 }); 27601 return; 27602 } 27603 this.manifest.mediaSequence = entry.number; 27604 }, 27605 'discontinuity-sequence': function discontinuitySequence() { 27606 if (!isFinite(entry.number)) { 27607 this.trigger('warn', { 27608 message: 'ignoring invalid discontinuity sequence: ' + entry.number 27609 }); 27610 return; 27611 } 27612 this.manifest.discontinuitySequence = entry.number; 27613 currentTimeline = entry.number; 27614 }, 27615 'playlist-type': function playlistType() { 27616 if (!/VOD|EVENT/.test(entry.playlistType)) { 27617 this.trigger('warn', { 27618 message: 'ignoring unknown playlist type: ' + entry.playlist 27619 }); 27620 return; 27621 } 27622 this.manifest.playlistType = entry.playlistType; 27623 }, 27624 map: function map() { 27625 currentMap = {}; 27626 if (entry.uri) { 27627 currentMap.uri = entry.uri; 27628 } 27629 if (entry.byterange) { 27630 currentMap.byterange = entry.byterange; 27631 } 27632 }, 27633 'stream-inf': function streamInf() { 27634 this.manifest.playlists = uris; 27635 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 27636 27637 if (!entry.attributes) { 27638 this.trigger('warn', { 27639 message: 'ignoring empty stream-inf attributes' 27640 }); 27641 return; 27642 } 27643 27644 if (!currentUri.attributes) { 27645 currentUri.attributes = {}; 27646 } 27647 _extends$1(currentUri.attributes, entry.attributes); 27648 }, 27649 media: function media() { 27650 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 27651 27652 if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { 27653 this.trigger('warn', { 27654 message: 'ignoring incomplete or missing media group' 27655 }); 27656 return; 27657 } 27658 27659 // find the media group, creating defaults as necessary 27660 var mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; 27661 27662 mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; 27663 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; 27664 27665 // collect the rendition metadata 27666 rendition = { 27667 'default': /yes/i.test(entry.attributes.DEFAULT) 27668 }; 27669 if (rendition['default']) { 27670 rendition.autoselect = true; 27671 } else { 27672 rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); 27673 } 27674 if (entry.attributes.LANGUAGE) { 27675 rendition.language = entry.attributes.LANGUAGE; 27676 } 27677 if (entry.attributes.URI) { 27678 rendition.uri = entry.attributes.URI; 27679 } 27680 if (entry.attributes['INSTREAM-ID']) { 27681 rendition.instreamId = entry.attributes['INSTREAM-ID']; 27682 } 27683 if (entry.attributes.CHARACTERISTICS) { 27684 rendition.characteristics = entry.attributes.CHARACTERISTICS; 27685 } 27686 if (entry.attributes.FORCED) { 27687 rendition.forced = /yes/i.test(entry.attributes.FORCED); 27688 } 27689 27690 // insert the new rendition 27691 mediaGroup[entry.attributes.NAME] = rendition; 27692 }, 27693 discontinuity: function discontinuity() { 27694 currentTimeline += 1; 27695 currentUri.discontinuity = true; 27696 this.manifest.discontinuityStarts.push(uris.length); 27697 }, 27698 'program-date-time': function programDateTime() { 27699 if (typeof this.manifest.dateTimeString === 'undefined') { 27700 // PROGRAM-DATE-TIME is a media-segment tag, but for backwards 27701 // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag 27702 // to the manifest object 27703 // TODO: Consider removing this in future major version 27704 this.manifest.dateTimeString = entry.dateTimeString; 27705 this.manifest.dateTimeObject = entry.dateTimeObject; 27706 } 27707 27708 currentUri.dateTimeString = entry.dateTimeString; 27709 currentUri.dateTimeObject = entry.dateTimeObject; 27710 }, 27711 targetduration: function targetduration() { 27712 if (!isFinite(entry.duration) || entry.duration < 0) { 27713 this.trigger('warn', { 27714 message: 'ignoring invalid target duration: ' + entry.duration 27715 }); 27716 return; 27717 } 27718 this.manifest.targetDuration = entry.duration; 27719 }, 27720 totalduration: function totalduration() { 27721 if (!isFinite(entry.duration) || entry.duration < 0) { 27722 this.trigger('warn', { 27723 message: 'ignoring invalid total duration: ' + entry.duration 27724 }); 27725 return; 27726 } 27727 this.manifest.totalDuration = entry.duration; 27728 }, 27729 start: function start() { 27730 if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { 27731 this.trigger('warn', { 27732 message: 'ignoring start declaration without appropriate attribute list' 27733 }); 27734 return; 27735 } 27736 this.manifest.start = { 27737 timeOffset: entry.attributes['TIME-OFFSET'], 27738 precise: entry.attributes.PRECISE 27739 }; 27740 }, 27741 'cue-out': function cueOut() { 27742 currentUri.cueOut = entry.data; 27743 }, 27744 'cue-out-cont': function cueOutCont() { 27745 currentUri.cueOutCont = entry.data; 27746 }, 27747 'cue-in': function cueIn() { 27748 currentUri.cueIn = entry.data; 27749 } 27750 })[entry.tagType] || noop).call(self); 27751 }, 27752 uri: function uri() { 27753 currentUri.uri = entry.uri; 27754 uris.push(currentUri); 27755 27756 // if no explicit duration was declared, use the target duration 27757 if (this.manifest.targetDuration && !('duration' in currentUri)) { 27758 this.trigger('warn', { 27759 message: 'defaulting segment duration to the target duration' 27760 }); 27761 currentUri.duration = this.manifest.targetDuration; 27762 } 27763 // annotate with encryption information, if necessary 27764 if (_key) { 27765 currentUri.key = _key; 27766 } 27767 currentUri.timeline = currentTimeline; 27768 // annotate with initialization segment information, if necessary 27769 if (currentMap) { 27770 currentUri.map = currentMap; 27771 } 27772 27773 // prepare for the next URI 27774 currentUri = {}; 27775 }, 27776 comment: function comment() { 27777 // comments are not important for playback 27778 }, 27779 custom: function custom() { 27780 // if this is segment-level data attach the output to the segment 27781 if (entry.segment) { 27782 currentUri.custom = currentUri.custom || {}; 27783 currentUri.custom[entry.customType] = entry.data; 27784 // if this is manifest-level data attach to the top level manifest object 27785 } else { 27786 this.manifest.custom = this.manifest.custom || {}; 27787 this.manifest.custom[entry.customType] = entry.data; 27788 } 27789 } 27790 })[entry.type].call(self); 27791 }); 27792 return _this; 27793 } 27794 27795 /** 27796 * Parse the input string and update the manifest object. 27797 * 27798 * @param {String} chunk a potentially incomplete portion of the manifest 27799 */ 27800 27801 Parser.prototype.push = function push(chunk) { 27802 this.lineStream.push(chunk); 27803 }; 27804 27805 /** 27806 * Flush any remaining input. This can be handy if the last line of an M3U8 27807 * manifest did not contain a trailing newline but the file has been 27808 * completely received. 27809 */ 27810 27811 Parser.prototype.end = function end() { 27812 // flush any buffered input 27813 this.lineStream.push('\n'); 27814 }; 27815 /** 27816 * Add an additional parser for non-standard tags 27817 * 27818 * @param {Object} options a map of options for the added parser 27819 * @param {RegExp} options.expression a regular expression to match the custom header 27820 * @param {string} options.type the type to register to the output 27821 * @param {Function} [options.dataParser] function to parse the line into an object 27822 * @param {boolean} [options.segment] should tag data be attached to the segment object 27823 */ 27824 27825 Parser.prototype.addParser = function addParser(options) { 27826 this.parseStream.addParser(options); 27827 }; 27828 27829 return Parser; 27830 }(Stream); 27831 27832 /** 27833 * mpd-parser 27834 * @version 0.6.1 27835 * @copyright 2018 Brightcove, Inc 27836 * @license Apache-2.0 27837 */ 27838 27839 var formatAudioPlaylist = function formatAudioPlaylist(_ref) { 27840 var _attributes; 27841 27842 var attributes = _ref.attributes, 27843 segments = _ref.segments; 27844 27845 var playlist = { 27846 attributes: (_attributes = { 27847 NAME: attributes.id, 27848 BANDWIDTH: attributes.bandwidth, 27849 CODECS: attributes.codecs 27850 }, _attributes['PROGRAM-ID'] = 1, _attributes), 27851 uri: '', 27852 endList: (attributes.type || 'static') === 'static', 27853 timeline: attributes.periodIndex, 27854 resolvedUri: '', 27855 targetDuration: attributes.duration, 27856 segments: segments, 27857 mediaSequence: segments.length ? segments[0].number : 1 27858 }; 27859 27860 if (attributes.contentProtection) { 27861 playlist.contentProtection = attributes.contentProtection; 27862 } 27863 27864 return playlist; 27865 }; 27866 27867 var formatVttPlaylist = function formatVttPlaylist(_ref2) { 27868 var _attributes2; 27869 27870 var attributes = _ref2.attributes, 27871 segments = _ref2.segments; 27872 27873 if (typeof segments === 'undefined') { 27874 // vtt tracks may use single file in BaseURL 27875 segments = [{ 27876 uri: attributes.baseUrl, 27877 timeline: attributes.periodIndex, 27878 resolvedUri: attributes.baseUrl || '', 27879 duration: attributes.sourceDuration, 27880 number: 0 27881 }]; 27882 // targetDuration should be the same duration as the only segment 27883 attributes.duration = attributes.sourceDuration; 27884 } 27885 return { 27886 attributes: (_attributes2 = { 27887 NAME: attributes.id, 27888 BANDWIDTH: attributes.bandwidth 27889 }, _attributes2['PROGRAM-ID'] = 1, _attributes2), 27890 uri: '', 27891 endList: (attributes.type || 'static') === 'static', 27892 timeline: attributes.periodIndex, 27893 resolvedUri: attributes.baseUrl || '', 27894 targetDuration: attributes.duration, 27895 segments: segments, 27896 mediaSequence: segments.length ? segments[0].number : 1 27897 }; 27898 }; 27899 27900 var organizeAudioPlaylists = function organizeAudioPlaylists(playlists) { 27901 return playlists.reduce(function (a, playlist) { 27902 var role = playlist.attributes.role && playlist.attributes.role.value || 'main'; 27903 var language = playlist.attributes.lang || ''; 27904 27905 var label = 'main'; 27906 27907 if (language) { 27908 label = playlist.attributes.lang + ' (' + role + ')'; 27909 } 27910 27911 // skip if we already have the highest quality audio for a language 27912 if (a[label] && a[label].playlists[0].attributes.BANDWIDTH > playlist.attributes.bandwidth) { 27913 return a; 27914 } 27915 27916 a[label] = { 27917 language: language, 27918 autoselect: true, 27919 'default': role === 'main', 27920 playlists: [formatAudioPlaylist(playlist)], 27921 uri: '' 27922 }; 27923 27924 return a; 27925 }, {}); 27926 }; 27927 27928 var organizeVttPlaylists = function organizeVttPlaylists(playlists) { 27929 return playlists.reduce(function (a, playlist) { 27930 var label = playlist.attributes.lang || 'text'; 27931 27932 // skip if we already have subtitles 27933 if (a[label]) { 27934 return a; 27935 } 27936 27937 a[label] = { 27938 language: label, 27939 'default': false, 27940 autoselect: false, 27941 playlists: [formatVttPlaylist(playlist)], 27942 uri: '' 27943 }; 27944 27945 return a; 27946 }, {}); 27947 }; 27948 27949 var formatVideoPlaylist = function formatVideoPlaylist(_ref3) { 27950 var _attributes3; 27951 27952 var attributes = _ref3.attributes, 27953 segments = _ref3.segments; 27954 27955 var playlist = { 27956 attributes: (_attributes3 = { 27957 NAME: attributes.id, 27958 AUDIO: 'audio', 27959 SUBTITLES: 'subs', 27960 RESOLUTION: { 27961 width: attributes.width, 27962 height: attributes.height 27963 }, 27964 CODECS: attributes.codecs, 27965 BANDWIDTH: attributes.bandwidth 27966 }, _attributes3['PROGRAM-ID'] = 1, _attributes3), 27967 uri: '', 27968 endList: (attributes.type || 'static') === 'static', 27969 timeline: attributes.periodIndex, 27970 resolvedUri: '', 27971 targetDuration: attributes.duration, 27972 segments: segments, 27973 mediaSequence: segments.length ? segments[0].number : 1 27974 }; 27975 27976 if (attributes.contentProtection) { 27977 playlist.contentProtection = attributes.contentProtection; 27978 } 27979 27980 return playlist; 27981 }; 27982 27983 var toM3u8 = function toM3u8(dashPlaylists) { 27984 var _mediaGroups; 27985 27986 if (!dashPlaylists.length) { 27987 return {}; 27988 } 27989 27990 // grab all master attributes 27991 var _dashPlaylists$0$attr = dashPlaylists[0].attributes, 27992 duration = _dashPlaylists$0$attr.sourceDuration, 27993 _dashPlaylists$0$attr2 = _dashPlaylists$0$attr.minimumUpdatePeriod, 27994 minimumUpdatePeriod = _dashPlaylists$0$attr2 === undefined ? 0 : _dashPlaylists$0$attr2; 27995 27996 var videoOnly = function videoOnly(_ref4) { 27997 var attributes = _ref4.attributes; 27998 return attributes.mimeType === 'video/mp4' || attributes.contentType === 'video'; 27999 }; 28000 var audioOnly = function audioOnly(_ref5) { 28001 var attributes = _ref5.attributes; 28002 return attributes.mimeType === 'audio/mp4' || attributes.contentType === 'audio'; 28003 }; 28004 var vttOnly = function vttOnly(_ref6) { 28005 var attributes = _ref6.attributes; 28006 return attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; 28007 }; 28008 28009 var videoPlaylists = dashPlaylists.filter(videoOnly).map(formatVideoPlaylist); 28010 var audioPlaylists = dashPlaylists.filter(audioOnly); 28011 var vttPlaylists = dashPlaylists.filter(vttOnly); 28012 28013 var master = { 28014 allowCache: true, 28015 discontinuityStarts: [], 28016 segments: [], 28017 endList: true, 28018 mediaGroups: (_mediaGroups = { 28019 AUDIO: {}, 28020 VIDEO: {} 28021 }, _mediaGroups['CLOSED-CAPTIONS'] = {}, _mediaGroups.SUBTITLES = {}, _mediaGroups), 28022 uri: '', 28023 duration: duration, 28024 playlists: videoPlaylists, 28025 minimumUpdatePeriod: minimumUpdatePeriod * 1000 28026 }; 28027 28028 if (audioPlaylists.length) { 28029 master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists); 28030 } 28031 28032 if (vttPlaylists.length) { 28033 master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists); 28034 } 28035 28036 return master; 28037 }; 28038 28039 var _typeof$1 = typeof Symbol === "function" && _typeof(Symbol.iterator) === "symbol" ? function (obj) { 28040 return typeof obj === 'undefined' ? 'undefined' : _typeof(obj); 28041 } : function (obj) { 28042 return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj === 'undefined' ? 'undefined' : _typeof(obj); 28043 }; 28044 28045 var isObject$1 = function isObject(obj) { 28046 return !!obj && (typeof obj === 'undefined' ? 'undefined' : _typeof$1(obj)) === 'object'; 28047 }; 28048 28049 var merge = function merge() { 28050 for (var _len = arguments.length, objects = Array(_len), _key = 0; _key < _len; _key++) { 28051 objects[_key] = arguments[_key]; 28052 } 28053 28054 return objects.reduce(function (result, source) { 28055 28056 Object.keys(source).forEach(function (key) { 28057 28058 if (Array.isArray(result[key]) && Array.isArray(source[key])) { 28059 result[key] = result[key].concat(source[key]); 28060 } else if (isObject$1(result[key]) && isObject$1(source[key])) { 28061 result[key] = merge(result[key], source[key]); 28062 } else { 28063 result[key] = source[key]; 28064 } 28065 }); 28066 return result; 28067 }, {}); 28068 }; 28069 28070 var resolveUrl = function resolveUrl(baseUrl, relativeUrl) { 28071 // return early if we don't need to resolve 28072 if (/^[a-z]+:/i.test(relativeUrl)) { 28073 return relativeUrl; 28074 } 28075 28076 // if the base URL is relative then combine with the current location 28077 if (!/\/\//i.test(baseUrl)) { 28078 baseUrl = urlToolkit.buildAbsoluteURL(window_1.location.href, baseUrl); 28079 } 28080 28081 return urlToolkit.buildAbsoluteURL(baseUrl, relativeUrl); 28082 }; 28083 28084 /** 28085 * @typedef {Object} SingleUri 28086 * @property {string} uri - relative location of segment 28087 * @property {string} resolvedUri - resolved location of segment 28088 * @property {Object} byterange - Object containing information on how to make byte range 28089 * requests following byte-range-spec per RFC2616. 28090 * @property {String} byterange.length - length of range request 28091 * @property {String} byterange.offset - byte offset of range request 28092 * 28093 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 28094 */ 28095 28096 /** 28097 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object 28098 * that conforms to how m3u8-parser is structured 28099 * 28100 * @see https://github.com/videojs/m3u8-parser 28101 * 28102 * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes 28103 * @param {string} source - source url for segment 28104 * @param {string} range - optional range used for range calls, follows 28105 * @return {SingleUri} full segment information transformed into a format similar 28106 * to m3u8-parser 28107 */ 28108 var urlTypeToSegment = function urlTypeToSegment(_ref) { 28109 var _ref$baseUrl = _ref.baseUrl, 28110 baseUrl = _ref$baseUrl === undefined ? '' : _ref$baseUrl, 28111 _ref$source = _ref.source, 28112 source = _ref$source === undefined ? '' : _ref$source, 28113 _ref$range = _ref.range, 28114 range = _ref$range === undefined ? '' : _ref$range; 28115 28116 var init = { 28117 uri: source, 28118 resolvedUri: resolveUrl(baseUrl || '', source) 28119 }; 28120 28121 if (range) { 28122 var ranges = range.split('-'); 28123 var startRange = parseInt(ranges[0], 10); 28124 var endRange = parseInt(ranges[1], 10); 28125 28126 init.byterange = { 28127 length: endRange - startRange, 28128 offset: startRange 28129 }; 28130 } 28131 28132 return init; 28133 }; 28134 28135 /** 28136 * Calculates the R (repetition) value for a live stream (for the final segment 28137 * in a manifest where the r value is negative 1) 28138 * 28139 * @param {Object} attributes 28140 * Object containing all inherited attributes from parent elements with attribute 28141 * names as keys 28142 * @param {number} time 28143 * current time (typically the total time up until the final segment) 28144 * @param {number} duration 28145 * duration property for the given <S /> 28146 * 28147 * @return {number} 28148 * R value to reach the end of the given period 28149 */ 28150 var getLiveRValue = function getLiveRValue(attributes, time, duration) { 28151 var NOW = attributes.NOW, 28152 clientOffset = attributes.clientOffset, 28153 availabilityStartTime = attributes.availabilityStartTime, 28154 _attributes$timescale = attributes.timescale, 28155 timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale, 28156 _attributes$start = attributes.start, 28157 start = _attributes$start === undefined ? 0 : _attributes$start, 28158 _attributes$minimumUp = attributes.minimumUpdatePeriod, 28159 minimumUpdatePeriod = _attributes$minimumUp === undefined ? 0 : _attributes$minimumUp; 28160 28161 var now = (NOW + clientOffset) / 1000; 28162 var periodStartWC = availabilityStartTime + start; 28163 var periodEndWC = now + minimumUpdatePeriod; 28164 var periodDuration = periodEndWC - periodStartWC; 28165 28166 return Math.ceil((periodDuration * timescale - time) / duration); 28167 }; 28168 28169 /** 28170 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment 28171 * timing and duration 28172 * 28173 * @param {Object} attributes 28174 * Object containing all inherited attributes from parent elements with attribute 28175 * names as keys 28176 * @param {Object[]} segmentTimeline 28177 * List of objects representing the attributes of each S element contained within 28178 * 28179 * @return {{number: number, duration: number, time: number, timeline: number}[]} 28180 * List of Objects with segment timing and duration info 28181 */ 28182 var parseByTimeline = function parseByTimeline(attributes, segmentTimeline) { 28183 var _attributes$type = attributes.type, 28184 type = _attributes$type === undefined ? 'static' : _attributes$type, 28185 _attributes$minimumUp2 = attributes.minimumUpdatePeriod, 28186 minimumUpdatePeriod = _attributes$minimumUp2 === undefined ? 0 : _attributes$minimumUp2, 28187 _attributes$media = attributes.media, 28188 media = _attributes$media === undefined ? '' : _attributes$media, 28189 sourceDuration = attributes.sourceDuration, 28190 _attributes$timescale2 = attributes.timescale, 28191 timescale = _attributes$timescale2 === undefined ? 1 : _attributes$timescale2, 28192 _attributes$startNumb = attributes.startNumber, 28193 startNumber = _attributes$startNumb === undefined ? 1 : _attributes$startNumb, 28194 timeline = attributes.periodIndex; 28195 28196 var segments = []; 28197 var time = -1; 28198 28199 for (var sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { 28200 var S = segmentTimeline[sIndex]; 28201 var duration = S.d; 28202 var repeat = S.r || 0; 28203 var segmentTime = S.t || 0; 28204 28205 if (time < 0) { 28206 // first segment 28207 time = segmentTime; 28208 } 28209 28210 if (segmentTime && segmentTime > time) { 28211 // discontinuity 28212 28213 // TODO: How to handle this type of discontinuity 28214 // timeline++ here would treat it like HLS discontuity and content would 28215 // get appended without gap 28216 // E.G. 28217 // <S t="0" d="1" /> 28218 // <S d="1" /> 28219 // <S d="1" /> 28220 // <S t="5" d="1" /> 28221 // would have $Time$ values of [0, 1, 2, 5] 28222 // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) 28223 // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) 28224 // does the value of sourceDuration consider this when calculating arbitrary 28225 // negative @r repeat value? 28226 // E.G. Same elements as above with this added at the end 28227 // <S d="1" r="-1" /> 28228 // with a sourceDuration of 10 28229 // Would the 2 gaps be included in the time duration calculations resulting in 28230 // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments 28231 // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? 28232 28233 time = segmentTime; 28234 } 28235 28236 var count = void 0; 28237 28238 if (repeat < 0) { 28239 var nextS = sIndex + 1; 28240 28241 if (nextS === segmentTimeline.length) { 28242 // last segment 28243 if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { 28244 count = getLiveRValue(attributes, time, duration); 28245 } else { 28246 // TODO: This may be incorrect depending on conclusion of TODO above 28247 count = (sourceDuration * timescale - time) / duration; 28248 } 28249 } else { 28250 count = (segmentTimeline[nextS].t - time) / duration; 28251 } 28252 } else { 28253 count = repeat + 1; 28254 } 28255 28256 var end = startNumber + segments.length + count; 28257 var number = startNumber + segments.length; 28258 28259 while (number < end) { 28260 segments.push({ number: number, duration: duration / timescale, time: time, timeline: timeline }); 28261 time += duration; 28262 number++; 28263 } 28264 } 28265 28266 return segments; 28267 }; 28268 28269 var range = function range(start, end) { 28270 var result = []; 28271 28272 for (var i = start; i < end; i++) { 28273 result.push(i); 28274 } 28275 28276 return result; 28277 }; 28278 28279 var flatten = function flatten(lists) { 28280 return lists.reduce(function (x, y) { 28281 return x.concat(y); 28282 }, []); 28283 }; 28284 28285 var from = function from(list) { 28286 if (!list.length) { 28287 return []; 28288 } 28289 28290 var result = []; 28291 28292 for (var i = 0; i < list.length; i++) { 28293 result.push(list[i]); 28294 } 28295 28296 return result; 28297 }; 28298 28299 /** 28300 * Functions for calculating the range of available segments in static and dynamic 28301 * manifests. 28302 */ 28303 var segmentRange = { 28304 /** 28305 * Returns the entire range of available segments for a static MPD 28306 * 28307 * @param {Object} attributes 28308 * Inheritied MPD attributes 28309 * @return {{ start: number, end: number }} 28310 * The start and end numbers for available segments 28311 */ 28312 'static': function _static(attributes) { 28313 var duration = attributes.duration, 28314 _attributes$timescale = attributes.timescale, 28315 timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale, 28316 sourceDuration = attributes.sourceDuration; 28317 28318 return { 28319 start: 0, 28320 end: Math.ceil(sourceDuration / (duration / timescale)) 28321 }; 28322 }, 28323 28324 /** 28325 * Returns the current live window range of available segments for a dynamic MPD 28326 * 28327 * @param {Object} attributes 28328 * Inheritied MPD attributes 28329 * @return {{ start: number, end: number }} 28330 * The start and end numbers for available segments 28331 */ 28332 dynamic: function dynamic(attributes) { 28333 var NOW = attributes.NOW, 28334 clientOffset = attributes.clientOffset, 28335 availabilityStartTime = attributes.availabilityStartTime, 28336 _attributes$timescale2 = attributes.timescale, 28337 timescale = _attributes$timescale2 === undefined ? 1 : _attributes$timescale2, 28338 duration = attributes.duration, 28339 _attributes$start = attributes.start, 28340 start = _attributes$start === undefined ? 0 : _attributes$start, 28341 _attributes$minimumUp = attributes.minimumUpdatePeriod, 28342 minimumUpdatePeriod = _attributes$minimumUp === undefined ? 0 : _attributes$minimumUp, 28343 _attributes$timeShift = attributes.timeShiftBufferDepth, 28344 timeShiftBufferDepth = _attributes$timeShift === undefined ? Infinity : _attributes$timeShift; 28345 28346 var now = (NOW + clientOffset) / 1000; 28347 var periodStartWC = availabilityStartTime + start; 28348 var periodEndWC = now + minimumUpdatePeriod; 28349 var periodDuration = periodEndWC - periodStartWC; 28350 var segmentCount = Math.ceil(periodDuration * timescale / duration); 28351 var availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); 28352 var availableEnd = Math.floor((now - periodStartWC) * timescale / duration); 28353 28354 return { 28355 start: Math.max(0, availableStart), 28356 end: Math.min(segmentCount, availableEnd) 28357 }; 28358 } 28359 }; 28360 28361 /** 28362 * Maps a range of numbers to objects with information needed to build the corresponding 28363 * segment list 28364 * 28365 * @name toSegmentsCallback 28366 * @function 28367 * @param {number} number 28368 * Number of the segment 28369 * @param {number} index 28370 * Index of the number in the range list 28371 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} 28372 * Object with segment timing and duration info 28373 */ 28374 28375 /** 28376 * Returns a callback for Array.prototype.map for mapping a range of numbers to 28377 * information needed to build the segment list. 28378 * 28379 * @param {Object} attributes 28380 * Inherited MPD attributes 28381 * @return {toSegmentsCallback} 28382 * Callback map function 28383 */ 28384 var toSegments = function toSegments(attributes) { 28385 return function (number, index) { 28386 var duration = attributes.duration, 28387 _attributes$timescale3 = attributes.timescale, 28388 timescale = _attributes$timescale3 === undefined ? 1 : _attributes$timescale3, 28389 periodIndex = attributes.periodIndex, 28390 _attributes$startNumb = attributes.startNumber, 28391 startNumber = _attributes$startNumb === undefined ? 1 : _attributes$startNumb; 28392 28393 return { 28394 number: startNumber + number, 28395 duration: duration / timescale, 28396 timeline: periodIndex, 28397 time: index * duration 28398 }; 28399 }; 28400 }; 28401 28402 /** 28403 * Returns a list of objects containing segment timing and duration info used for 28404 * building the list of segments. This uses the @duration attribute specified 28405 * in the MPD manifest to derive the range of segments. 28406 * 28407 * @param {Object} attributes 28408 * Inherited MPD attributes 28409 * @return {{number: number, duration: number, time: number, timeline: number}[]} 28410 * List of Objects with segment timing and duration info 28411 */ 28412 var parseByDuration = function parseByDuration(attributes) { 28413 var _attributes$type = attributes.type, 28414 type = _attributes$type === undefined ? 'static' : _attributes$type, 28415 duration = attributes.duration, 28416 _attributes$timescale4 = attributes.timescale, 28417 timescale = _attributes$timescale4 === undefined ? 1 : _attributes$timescale4, 28418 sourceDuration = attributes.sourceDuration; 28419 28420 var _segmentRange$type = segmentRange[type](attributes), 28421 start = _segmentRange$type.start, 28422 end = _segmentRange$type.end; 28423 28424 var segments = range(start, end).map(toSegments(attributes)); 28425 28426 if (type === 'static') { 28427 var index = segments.length - 1; 28428 28429 // final segment may be less than full segment duration 28430 segments[index].duration = sourceDuration - duration / timescale * index; 28431 } 28432 28433 return segments; 28434 }; 28435 28436 var identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; 28437 28438 /** 28439 * Replaces template identifiers with corresponding values. To be used as the callback 28440 * for String.prototype.replace 28441 * 28442 * @name replaceCallback 28443 * @function 28444 * @param {string} match 28445 * Entire match of identifier 28446 * @param {string} identifier 28447 * Name of matched identifier 28448 * @param {string} format 28449 * Format tag string. Its presence indicates that padding is expected 28450 * @param {string} width 28451 * Desired length of the replaced value. Values less than this width shall be left 28452 * zero padded 28453 * @return {string} 28454 * Replacement for the matched identifier 28455 */ 28456 28457 /** 28458 * Returns a function to be used as a callback for String.prototype.replace to replace 28459 * template identifiers 28460 * 28461 * @param {Obect} values 28462 * Object containing values that shall be used to replace known identifiers 28463 * @param {number} values.RepresentationID 28464 * Value of the Representation@id attribute 28465 * @param {number} values.Number 28466 * Number of the corresponding segment 28467 * @param {number} values.Bandwidth 28468 * Value of the Representation@bandwidth attribute. 28469 * @param {number} values.Time 28470 * Timestamp value of the corresponding segment 28471 * @return {replaceCallback} 28472 * Callback to be used with String.prototype.replace to replace identifiers 28473 */ 28474 var identifierReplacement = function identifierReplacement(values) { 28475 return function (match, identifier, format, width) { 28476 if (match === '$$') { 28477 // escape sequence 28478 return '$'; 28479 } 28480 28481 if (typeof values[identifier] === 'undefined') { 28482 return match; 28483 } 28484 28485 var value = '' + values[identifier]; 28486 28487 if (identifier === 'RepresentationID') { 28488 // Format tag shall not be present with RepresentationID 28489 return value; 28490 } 28491 28492 if (!format) { 28493 width = 1; 28494 } else { 28495 width = parseInt(width, 10); 28496 } 28497 28498 if (value.length >= width) { 28499 return value; 28500 } 28501 28502 return '' + new Array(width - value.length + 1).join('0') + value; 28503 }; 28504 }; 28505 28506 /** 28507 * Constructs a segment url from a template string 28508 * 28509 * @param {string} url 28510 * Template string to construct url from 28511 * @param {Obect} values 28512 * Object containing values that shall be used to replace known identifiers 28513 * @param {number} values.RepresentationID 28514 * Value of the Representation@id attribute 28515 * @param {number} values.Number 28516 * Number of the corresponding segment 28517 * @param {number} values.Bandwidth 28518 * Value of the Representation@bandwidth attribute. 28519 * @param {number} values.Time 28520 * Timestamp value of the corresponding segment 28521 * @return {string} 28522 * Segment url with identifiers replaced 28523 */ 28524 var constructTemplateUrl = function constructTemplateUrl(url, values) { 28525 return url.replace(identifierPattern, identifierReplacement(values)); 28526 }; 28527 28528 /** 28529 * Generates a list of objects containing timing and duration information about each 28530 * segment needed to generate segment uris and the complete segment object 28531 * 28532 * @param {Object} attributes 28533 * Object containing all inherited attributes from parent elements with attribute 28534 * names as keys 28535 * @param {Object[]|undefined} segmentTimeline 28536 * List of objects representing the attributes of each S element contained within 28537 * the SegmentTimeline element 28538 * @return {{number: number, duration: number, time: number, timeline: number}[]} 28539 * List of Objects with segment timing and duration info 28540 */ 28541 var parseTemplateInfo = function parseTemplateInfo(attributes, segmentTimeline) { 28542 if (!attributes.duration && !segmentTimeline) { 28543 // if neither @duration or SegmentTimeline are present, then there shall be exactly 28544 // one media segment 28545 return [{ 28546 number: attributes.startNumber || 1, 28547 duration: attributes.sourceDuration, 28548 time: 0, 28549 timeline: attributes.periodIndex 28550 }]; 28551 } 28552 28553 if (attributes.duration) { 28554 return parseByDuration(attributes); 28555 } 28556 28557 return parseByTimeline(attributes, segmentTimeline); 28558 }; 28559 28560 /** 28561 * Generates a list of segments using information provided by the SegmentTemplate element 28562 * 28563 * @param {Object} attributes 28564 * Object containing all inherited attributes from parent elements with attribute 28565 * names as keys 28566 * @param {Object[]|undefined} segmentTimeline 28567 * List of objects representing the attributes of each S element contained within 28568 * the SegmentTimeline element 28569 * @return {Object[]} 28570 * List of segment objects 28571 */ 28572 var segmentsFromTemplate = function segmentsFromTemplate(attributes, segmentTimeline) { 28573 var templateValues = { 28574 RepresentationID: attributes.id, 28575 Bandwidth: attributes.bandwidth || 0 28576 }; 28577 28578 var _attributes$initializ = attributes.initialization, 28579 initialization = _attributes$initializ === undefined ? { sourceURL: '', range: '' } : _attributes$initializ; 28580 28581 var mapSegment = urlTypeToSegment({ 28582 baseUrl: attributes.baseUrl, 28583 source: constructTemplateUrl(initialization.sourceURL, templateValues), 28584 range: initialization.range 28585 }); 28586 28587 var segments = parseTemplateInfo(attributes, segmentTimeline); 28588 28589 return segments.map(function (segment) { 28590 templateValues.Number = segment.number; 28591 templateValues.Time = segment.time; 28592 28593 var uri = constructTemplateUrl(attributes.media || '', templateValues); 28594 28595 return { 28596 uri: uri, 28597 timeline: segment.timeline, 28598 duration: segment.duration, 28599 resolvedUri: resolveUrl(attributes.baseUrl || '', uri), 28600 map: mapSegment, 28601 number: segment.number 28602 }; 28603 }); 28604 }; 28605 28606 var errors = {
28607 INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', 28608 DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', 28609 DASH_INVALID_XML: 'DASH_INVALID_XML', 28610 NO_BASE_URL: 'NO_BASE_URL', 28611 MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', 28612 SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', 28613 UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' 28614 }; 28615 28616 /** 28617 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14) 28618 * to an object that matches the output of a segment in videojs/mpd-parser 28619 * 28620 * @param {Object} attributes 28621 * Object containing all inherited attributes from parent elements with attribute 28622 * names as keys 28623 * @param {Object} segmentUrl 28624 * <SegmentURL> node to translate into a segment object 28625 * @return {Object} translated segment object 28626 */ 28627 var SegmentURLToSegmentObject = function SegmentURLToSegmentObject(attributes, segmentUrl) { 28628 var baseUrl = attributes.baseUrl, 28629 _attributes$initializ = attributes.initialization, 28630 initialization = _attributes$initializ === undefined ? {} : _attributes$initializ; 28631 28632 var initSegment = urlTypeToSegment({ 28633 baseUrl: baseUrl, 28634 source: initialization.sourceURL, 28635 range: initialization.range 28636 }); 28637 28638 var segment = urlTypeToSegment({ 28639 baseUrl: baseUrl, 28640 source: segmentUrl.media, 28641 range: segmentUrl.mediaRange 28642 }); 28643 28644 segment.map = initSegment; 28645 28646 return segment; 28647 }; 28648 28649 /** 28650 * Generates a list of segments using information provided by the SegmentList element 28651 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 28652 * node should be translated into segment. 28653 * 28654 * @param {Object} attributes 28655 * Object containing all inherited attributes from parent elements with attribute 28656 * names as keys 28657 * @param {Object[]|undefined} segmentTimeline 28658 * List of objects representing the attributes of each S element contained within 28659 * the SegmentTimeline element 28660 * @return {Object.<Array>} list of segments 28661 */
vendor: 18,658 bytes, lines 28662-29215
28662 var segmentsFromList = function segmentsFromList(attributes, segmentTimeline) { 28663 var duration = attributes.duration, 28664 _attributes$segmentUr = attributes.segmentUrls, 28665 segmentUrls = _attributes$segmentUr === undefined ? [] : _attributes$segmentUr; 28666 28667 // Per spec (5.3.9.2.1) no way to determine segment duration OR 28668 // if both SegmentTimeline and @duration are defined, it is outside of spec. 28669 28670 if (!duration && !segmentTimeline || duration && segmentTimeline) { 28671 throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); 28672 } 28673 28674 var segmentUrlMap = segmentUrls.map(function (segmentUrlObject) { 28675 return SegmentURLToSegmentObject(attributes, segmentUrlObject); 28676 }); 28677 var segmentTimeInfo = void 0; 28678 28679 if (duration) { 28680 segmentTimeInfo = parseByDuration(attributes); 28681 } 28682 28683 if (segmentTimeline) { 28684 segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); 28685 } 28686 28687 var segments = segmentTimeInfo.map(function (segmentTime, index) { 28688 if (segmentUrlMap[index]) { 28689 var segment = segmentUrlMap[index]; 28690 28691 segment.timeline = segmentTime.timeline; 28692 segment.duration = segmentTime.duration; 28693 segment.number = segmentTime.number; 28694 return segment; 28695 } 28696 // Since we're mapping we should get rid of any blank segments (in case 28697 // the given SegmentTimeline is handling for more elements than we have 28698 // SegmentURLs for). 28699 }).filter(function (segment) { 28700 return segment; 28701 }); 28702 28703 return segments; 28704 }; 28705 28706 /** 28707 * Translates SegmentBase into a set of segments. 28708 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 28709 * node should be translated into segment. 28710 * 28711 * @param {Object} attributes 28712 * Object containing all inherited attributes from parent elements with attribute 28713 * names as keys 28714 * @return {Object.<Array>} list of segments 28715 */ 28716 var segmentsFromBase = function segmentsFromBase(attributes) { 28717 var baseUrl = attributes.baseUrl, 28718 _attributes$initializ = attributes.initialization, 28719 initialization = _attributes$initializ === undefined ? {} : _attributes$initializ, 28720 sourceDuration = attributes.sourceDuration, 28721 _attributes$timescale = attributes.timescale, 28722 timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale, 28723 _attributes$indexRang = attributes.indexRange, 28724 indexRange = _attributes$indexRang === undefined ? '' : _attributes$indexRang, 28725 duration = attributes.duration; 28726 28727 // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) 28728 28729 if (!baseUrl) { 28730 throw new Error(errors.NO_BASE_URL); 28731 } 28732 28733 var initSegment = urlTypeToSegment({ 28734 baseUrl: baseUrl, 28735 source: initialization.sourceURL, 28736 range: initialization.range 28737 }); 28738 var segment = urlTypeToSegment({ baseUrl: baseUrl, source: baseUrl, range: indexRange }); 28739 28740 segment.map = initSegment; 28741 28742 // If there is a duration, use it, otherwise use the given duration of the source 28743 // (since SegmentBase is only for one total segment) 28744 if (duration) { 28745 var segmentTimeInfo = parseByDuration(attributes); 28746 28747 if (segmentTimeInfo.length) { 28748 segment.duration = segmentTimeInfo[0].duration; 28749 segment.timeline = segmentTimeInfo[0].timeline; 28750 } 28751 } else if (sourceDuration) { 28752 segment.duration = sourceDuration / timescale; 28753 segment.timeline = 0; 28754 } 28755 28756 // This is used for mediaSequence 28757 segment.number = 0; 28758 28759 return [segment]; 28760 }; 28761 28762 var generateSegments = function generateSegments(_ref) { 28763 var attributes = _ref.attributes, 28764 segmentInfo = _ref.segmentInfo; 28765 28766 var segmentAttributes = void 0; 28767 var segmentsFn = void 0; 28768 28769 if (segmentInfo.template) { 28770 segmentsFn = segmentsFromTemplate; 28771 segmentAttributes = merge(attributes, segmentInfo.template); 28772 } else if (segmentInfo.base) { 28773 segmentsFn = segmentsFromBase; 28774 segmentAttributes = merge(attributes, segmentInfo.base); 28775 } else if (segmentInfo.list) { 28776 segmentsFn = segmentsFromList; 28777 segmentAttributes = merge(attributes, segmentInfo.list); 28778 } 28779 28780 if (!segmentsFn) { 28781 return { attributes: attributes }; 28782 } 28783 28784 var segments = segmentsFn(segmentAttributes, segmentInfo.timeline); 28785 28786 // The @duration attribute will be used to determin the playlist's targetDuration which 28787 // must be in seconds. Since we've generated the segment list, we no longer need 28788 // @duration to be in @timescale units, so we can convert it here. 28789 if (segmentAttributes.duration) { 28790 var _segmentAttributes = segmentAttributes, 28791 duration = _segmentAttributes.duration, 28792 _segmentAttributes$ti = _segmentAttributes.timescale, 28793 timescale = _segmentAttributes$ti === undefined ? 1 : _segmentAttributes$ti; 28794 28795 segmentAttributes.duration = duration / timescale; 28796 } else if (segments.length) { 28797 // if there is no @duration attribute, use the largest segment duration as 28798 // as target duration 28799 segmentAttributes.duration = segments.reduce(function (max, segment) { 28800 return Math.max(max, Math.ceil(segment.duration)); 28801 }, 0); 28802 } else { 28803 segmentAttributes.duration = 0; 28804 } 28805 28806 return { 28807 attributes: segmentAttributes, 28808 segments: segments 28809 }; 28810 }; 28811 28812 var toPlaylists = function toPlaylists(representations) { 28813 return representations.map(generateSegments); 28814 }; 28815 28816 var findChildren = function findChildren(element, name) { 28817 return from(element.childNodes).filter(function (_ref) { 28818 var tagName = _ref.tagName; 28819 return tagName === name; 28820 }); 28821 }; 28822 28823 var getContent = function getContent(element) { 28824 return element.textContent.trim(); 28825 }; 28826 28827 var parseDuration = function parseDuration(str) { 28828 var SECONDS_IN_YEAR = 365 * 24 * 60 * 60; 28829 var SECONDS_IN_MONTH = 30 * 24 * 60 * 60; 28830 var SECONDS_IN_DAY = 24 * 60 * 60; 28831 var SECONDS_IN_HOUR = 60 * 60; 28832 var SECONDS_IN_MIN = 60; 28833 28834 // P10Y10M10DT10H10M10.1S 28835 var durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; 28836 var match = durationRegex.exec(str); 28837 28838 if (!match) { 28839 return 0; 28840 } 28841 28842 var _match$slice = match.slice(1), 28843 year = _match$slice[0], 28844 month = _match$slice[1], 28845 day = _match$slice[2], 28846 hour = _match$slice[3], 28847 minute = _match$slice[4], 28848 second = _match$slice[5]; 28849 28850 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); 28851 }; 28852 28853 var parseDate = function parseDate(str) { 28854 // Date format without timezone according to ISO 8601 28855 // YYY-MM-DDThh:mm:ss.ssssss 28856 var dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; 28857 28858 // If the date string does not specifiy a timezone, we must specifiy UTC. This is 28859 // expressed by ending with 'Z' 28860 if (dateRegex.test(str)) { 28861 str += 'Z'; 28862 } 28863 28864 return Date.parse(str); 28865 }; 28866 28867 // TODO: maybe order these in some way that makes it easy to find specific attributes 28868 var parsers = { 28869 /** 28870 * Specifies the duration of the entire Media Presentation. Format is a duration string 28871 * as specified in ISO 8601 28872 * 28873 * @param {string} value 28874 * value of attribute as a string 28875 * @return {number} 28876 * The duration in seconds 28877 */ 28878 mediaPresentationDuration: function mediaPresentationDuration(value) { 28879 return parseDuration(value); 28880 }, 28881 28882 /** 28883 * Specifies the Segment availability start time for all Segments referred to in this 28884 * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability 28885 * time. Format is a date string as specified in ISO 8601 28886 * 28887 * @param {string} value 28888 * value of attribute as a string 28889 * @return {number} 28890 * The date as seconds from unix epoch 28891 */ 28892 availabilityStartTime: function availabilityStartTime(value) { 28893 return parseDate(value) / 1000; 28894 }, 28895 28896 /** 28897 * Specifies the smallest period between potential changes to the MPD. Format is a 28898 * duration string as specified in ISO 8601 28899 * 28900 * @param {string} value 28901 * value of attribute as a string 28902 * @return {number} 28903 * The duration in seconds 28904 */ 28905 minimumUpdatePeriod: function minimumUpdatePeriod(value) { 28906 return parseDuration(value); 28907 }, 28908 28909 /** 28910 * Specifies the duration of the smallest time shifting buffer for any Representation 28911 * in the MPD. Format is a duration string as specified in ISO 8601 28912 * 28913 * @param {string} value 28914 * value of attribute as a string 28915 * @return {number} 28916 * The duration in seconds 28917 */ 28918 timeShiftBufferDepth: function timeShiftBufferDepth(value) { 28919 return parseDuration(value); 28920 }, 28921 28922 /** 28923 * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. 28924 * Format is a duration string as specified in ISO 8601 28925 * 28926 * @param {string} value 28927 * value of attribute as a string 28928 * @return {number} 28929 * The duration in seconds 28930 */ 28931 start: function start(value) { 28932 return parseDuration(value); 28933 }, 28934 28935 /** 28936 * Specifies the width of the visual presentation 28937 * 28938 * @param {string} value 28939 * value of attribute as a string 28940 * @return {number} 28941 * The parsed width 28942 */ 28943 width: function width(value) { 28944 return parseInt(value, 10); 28945 }, 28946 28947 /** 28948 * Specifies the height of the visual presentation 28949 * 28950 * @param {string} value 28951 * value of attribute as a string 28952 * @return {number} 28953 * The parsed height 28954 */ 28955 height: function height(value) { 28956 return parseInt(value, 10); 28957 }, 28958 28959 /** 28960 * Specifies the bitrate of the representation 28961 * 28962 * @param {string} value 28963 * value of attribute as a string 28964 * @return {number} 28965 * The parsed bandwidth 28966 */ 28967 bandwidth: function bandwidth(value) { 28968 return parseInt(value, 10); 28969 }, 28970 28971 /** 28972 * Specifies the number of the first Media Segment in this Representation in the Period 28973 * 28974 * @param {string} value 28975 * value of attribute as a string 28976 * @return {number} 28977 * The parsed number 28978 */ 28979 startNumber: function startNumber(value) { 28980 return parseInt(value, 10); 28981 }, 28982 28983 /** 28984 * Specifies the timescale in units per seconds 28985 * 28986 * @param {string} value 28987 * value of attribute as a string 28988 * @return {number} 28989 * The aprsed timescale 28990 */ 28991 timescale: function timescale(value) { 28992 return parseInt(value, 10); 28993 }, 28994 28995 /** 28996 * Specifies the constant approximate Segment duration 28997 * NOTE: The <Period> element also contains an @duration attribute. This duration 28998 * specifies the duration of the Period. This attribute is currently not 28999 * supported by the rest of the parser, however we still check for it to prevent 29000 * errors. 29001 * 29002 * @param {string} value 29003 * value of attribute as a string 29004 * @return {number} 29005 * The parsed duration 29006 */ 29007 duration: function duration(value) { 29008 var parsedValue = parseInt(value, 10); 29009 29010 if (isNaN(parsedValue)) { 29011 return parseDuration(value); 29012 } 29013 29014 return parsedValue; 29015 }, 29016 29017 /** 29018 * Specifies the Segment duration, in units of the value of the @timescale. 29019 * 29020 * @param {string} value 29021 * value of attribute as a string 29022 * @return {number} 29023 * The parsed duration 29024 */ 29025 d: function d(value) { 29026 return parseInt(value, 10); 29027 }, 29028 29029 /** 29030 * Specifies the MPD start time, in @timescale units, the first Segment in the series 29031 * starts relative to the beginning of the Period 29032 * 29033 * @param {string} value 29034 * value of attribute as a string 29035 * @return {number} 29036 * The parsed time 29037 */ 29038 t: function t(value) { 29039 return parseInt(value, 10); 29040 }, 29041 29042 /** 29043 * Specifies the repeat count of the number of following contiguous Segments with the 29044 * same duration expressed by the value of @d 29045 * 29046 * @param {string} value 29047 * value of attribute as a string 29048 * @return {number} 29049 * The parsed number 29050 */ 29051 r: function r(value) { 29052 return parseInt(value, 10); 29053 }, 29054 29055 /** 29056 * Default parser for all other attributes. Acts as a no-op and just returns the value 29057 * as a string 29058 * 29059 * @param {string} value 29060 * value of attribute as a string 29061 * @return {string} 29062 * Unparsed value 29063 */ 29064 DEFAULT: function DEFAULT(value) { 29065 return value; 29066 } 29067 }; 29068 29069 /** 29070 * Gets all the attributes and values of the provided node, parses attributes with known 29071 * types, and returns an object with attribute names mapped to values. 29072 * 29073 * @param {Node} el 29074 * The node to parse attributes from 29075 * @return {Object} 29076 * Object with all attributes of el parsed 29077 */ 29078 var parseAttributes$1 = function parseAttributes(el) { 29079 if (!(el && el.attributes)) { 29080 return {}; 29081 } 29082 29083 return from(el.attributes).reduce(function (a, e) { 29084 var parseFn = parsers[e.name] || parsers.DEFAULT; 29085 29086 a[e.name] = parseFn(e.value); 29087 29088 return a; 29089 }, {}); 29090 }; 29091 29092 function decodeB64ToUint8Array(b64Text) { 29093 var decodedString = window_1.atob(b64Text); 29094 var array = new Uint8Array(decodedString.length); 29095 29096 for (var i = 0; i < decodedString.length; i++) { 29097 array[i] = decodedString.charCodeAt(i); 29098 } 29099 return array; 29100 } 29101 29102 var keySystemsMap = { 29103 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', 29104 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', 29105 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', 29106 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime' 29107 }; 29108 29109 /** 29110 * Builds a list of urls that is the product of the reference urls and BaseURL values 29111 * 29112 * @param {string[]} referenceUrls 29113 * List of reference urls to resolve to 29114 * @param {Node[]} baseUrlElements 29115 * List of BaseURL nodes from the mpd 29116 * @return {string[]} 29117 * List of resolved urls 29118 */ 29119 var buildBaseUrls = function buildBaseUrls(referenceUrls, baseUrlElements) { 29120 if (!baseUrlElements.length) { 29121 return referenceUrls; 29122 } 29123 29124 return flatten(referenceUrls.map(function (reference) { 29125 return baseUrlElements.map(function (baseUrlElement) { 29126 return resolveUrl(reference, getContent(baseUrlElement)); 29127 }); 29128 })); 29129 }; 29130 29131 /** 29132 * Contains all Segment information for its containing AdaptationSet 29133 * 29134 * @typedef {Object} SegmentInformation 29135 * @property {Object|undefined} template 29136 * Contains the attributes for the SegmentTemplate node 29137 * @property {Object[]|undefined} timeline 29138 * Contains a list of atrributes for each S node within the SegmentTimeline node 29139 * @property {Object|undefined} list 29140 * Contains the attributes for the SegmentList node 29141 * @property {Object|undefined} base 29142 * Contains the attributes for the SegmentBase node 29143 */ 29144 29145 /** 29146 * Returns all available Segment information contained within the AdaptationSet node 29147 * 29148 * @param {Node} adaptationSet 29149 * The AdaptationSet node to get Segment information from 29150 * @return {SegmentInformation} 29151 * The Segment information contained within the provided AdaptationSet 29152 */ 29153 var getSegmentInformation = function getSegmentInformation(adaptationSet) { 29154 var segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; 29155 var segmentList = findChildren(adaptationSet, 'SegmentList')[0]; 29156 var segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(function (s) { 29157 return merge({ tag: 'SegmentURL' }, parseAttributes$1(s)); 29158 }); 29159 var segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; 29160 var segmentTimelineParentNode = segmentList || segmentTemplate; 29161 var segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; 29162 var segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; 29163 var segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; 29164 29165 // SegmentTemplate is handled slightly differently, since it can have both 29166 // @initialization and an <Initialization> node. @initialization can be templated, 29167 // while the node can have a url and range specified. If the <SegmentTemplate> has 29168 // both @initialization and an <Initialization> subelement we opt to override with 29169 // the node, as this interaction is not defined in the spec. 29170 var template = segmentTemplate && parseAttributes$1(segmentTemplate); 29171 29172 if (template && segmentInitialization) { 29173 template.initialization = segmentInitialization && parseAttributes$1(segmentInitialization); 29174 } else if (template && template.initialization) { 29175 // If it is @initialization we convert it to an object since this is the format that 29176 // later functions will rely on for the initialization segment. This is only valid 29177 // for <SegmentTemplate> 29178 template.initialization = { sourceURL: template.initialization }; 29179 } 29180 29181 var segmentInfo = { 29182 template: template, 29183 timeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(function (s) { 29184 return parseAttributes$1(s); 29185 }), 29186 list: segmentList && merge(parseAttributes$1(segmentList), { 29187 segmentUrls: segmentUrls, 29188 initialization: parseAttributes$1(segmentInitialization) 29189 }), 29190 base: segmentBase && merge(parseAttributes$1(segmentBase), { 29191 initialization: parseAttributes$1(segmentInitialization) 29192 }) 29193 }; 29194 29195 Object.keys(segmentInfo).forEach(function (key) { 29196 if (!segmentInfo[key]) { 29197 delete segmentInfo[key]; 29198 } 29199 }); 29200 29201 return segmentInfo; 29202 }; 29203 29204 /** 29205 * Contains Segment information and attributes needed to construct a Playlist object 29206 * from a Representation 29207 * 29208 * @typedef {Object} RepresentationInformation 29209 * @property {SegmentInformation} segmentInfo 29210 * Segment information for this Representation 29211 * @property {Object} attributes 29212 * Inherited attributes for this Representation 29213 */ 29214 29215 /**
29216 * Maps a Representation node to an object containing Segment information and attributes 29217 * 29218 * @name inheritBaseUrlsCallback 29219 * @function 29220 * @param {Node} representation 29221 * Representation node from the mpd 29222 * @return {RepresentationInformation} 29223 * Representation information needed to construct a Playlist object 29224 */ 29225 29226 /** 29227 * Returns a callback for Array.prototype.map for mapping Representation nodes to 29228 * Segment information and attributes using inherited BaseURL nodes. 29229 * 29230 * @param {Object} adaptationSetAttributes 29231 * Contains attributes inherited by the AdaptationSet 29232 * @param {string[]} adaptationSetBaseUrls 29233 * Contains list of resolved base urls inherited by the AdaptationSet 29234 * @param {SegmentInformation} adaptationSetSegmentInfo 29235 * Contains Segment information for the AdaptationSet 29236 * @return {inheritBaseUrlsCallback} 29237 * Callback map function 29238 */ 29239 var inheritBaseUrls = function inheritBaseUrls(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) { 29240 return function (representation) { 29241 var repBaseUrlElements = findChildren(representation, 'BaseURL'); 29242 var repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); 29243 var attributes = merge(adaptationSetAttributes, parseAttributes$1(representation)); 29244 var representationSegmentInfo = getSegmentInformation(representation); 29245 29246 return repBaseUrls.map(function (baseUrl) { 29247 return { 29248 segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo), 29249 attributes: merge(attributes, { baseUrl: baseUrl }) 29250 }; 29251 }); 29252 }; 29253 }; 29254 29255 /** 29256 * Tranforms a series of content protection nodes to
vendor: 15,888 bytes, lines 29257-29721
29257 * an object containing pssh data by key system 29258 * 29259 * @param {Node[]} contentProtectionNodes 29260 * Content protection nodes 29261 * @return {Object} 29262 * Object containing pssh data by key system 29263 */ 29264 var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) { 29265 return contentProtectionNodes.reduce(function (acc, node) { 29266 var attributes = parseAttributes$1(node); 29267 var keySystem = keySystemsMap[attributes.schemeIdUri]; 29268 29269 if (keySystem) { 29270 acc[keySystem] = { attributes: attributes }; 29271 29272 var psshNode = findChildren(node, 'cenc:pssh')[0]; 29273 29274 if (psshNode) { 29275 var pssh = getContent(psshNode); 29276 var psshBuffer = pssh && decodeB64ToUint8Array(pssh); 29277 29278 acc[keySystem].pssh = psshBuffer; 29279 } 29280 } 29281 29282 return acc; 29283 }, {}); 29284 }; 29285 29286 /** 29287 * Maps an AdaptationSet node to a list of Representation information objects 29288 * 29289 * @name toRepresentationsCallback 29290 * @function 29291 * @param {Node} adaptationSet 29292 * AdaptationSet node from the mpd 29293 * @return {RepresentationInformation[]} 29294 * List of objects containing Representaion information 29295 */ 29296 29297 /** 29298 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of 29299 * Representation information objects 29300 * 29301 * @param {Object} periodAttributes 29302 * Contains attributes inherited by the Period 29303 * @param {string[]} periodBaseUrls 29304 * Contains list of resolved base urls inherited by the Period 29305 * @param {string[]} periodSegmentInfo 29306 * Contains Segment Information at the period level 29307 * @return {toRepresentationsCallback} 29308 * Callback map function 29309 */ 29310 var toRepresentations = function toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo) { 29311 return function (adaptationSet) { 29312 var adaptationSetAttributes = parseAttributes$1(adaptationSet); 29313 var adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); 29314 var role = findChildren(adaptationSet, 'Role')[0]; 29315 var roleAttributes = { role: parseAttributes$1(role) }; 29316 29317 var attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes); 29318 29319 var contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); 29320 29321 if (Object.keys(contentProtection).length) { 29322 attrs = merge(attrs, { contentProtection: contentProtection }); 29323 } 29324 29325 var segmentInfo = getSegmentInformation(adaptationSet); 29326 var representations = findChildren(adaptationSet, 'Representation'); 29327 var adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo); 29328 29329 return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); 29330 }; 29331 }; 29332 29333 /** 29334 * Maps an Period node to a list of Representation inforamtion objects for all 29335 * AdaptationSet nodes contained within the Period 29336 * 29337 * @name toAdaptationSetsCallback 29338 * @function 29339 * @param {Node} period 29340 * Period node from the mpd 29341 * @param {number} periodIndex 29342 * Index of the Period within the mpd 29343 * @return {RepresentationInformation[]} 29344 * List of objects containing Representaion information 29345 */ 29346 29347 /** 29348 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of 29349 * Representation information objects 29350 * 29351 * @param {Object} mpdAttributes 29352 * Contains attributes inherited by the mpd 29353 * @param {string[]} mpdBaseUrls 29354 * Contains list of resolved base urls inherited by the mpd 29355 * @return {toAdaptationSetsCallback} 29356 * Callback map function 29357 */ 29358 var toAdaptationSets = function toAdaptationSets(mpdAttributes, mpdBaseUrls) { 29359 return function (period, periodIndex) { 29360 var periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL')); 29361 var periodAtt = parseAttributes$1(period); 29362 var periodAttributes = merge(mpdAttributes, periodAtt, { periodIndex: periodIndex }); 29363 var adaptationSets = findChildren(period, 'AdaptationSet'); 29364 var periodSegmentInfo = getSegmentInformation(period); 29365 29366 return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); 29367 }; 29368 }; 29369 29370 /** 29371 * Traverses the mpd xml tree to generate a list of Representation information objects 29372 * that have inherited attributes from parent nodes 29373 * 29374 * @param {Node} mpd 29375 * The root node of the mpd 29376 * @param {Object} options 29377 * Available options for inheritAttributes 29378 * @param {string} options.manifestUri 29379 * The uri source of the mpd 29380 * @param {number} options.NOW 29381 * Current time per DASH IOP. Default is current time in ms since epoch 29382 * @param {number} options.clientOffset 29383 * Client time difference from NOW (in milliseconds) 29384 * @return {RepresentationInformation[]} 29385 * List of objects containing Representation information 29386 */ 29387 var inheritAttributes = function inheritAttributes(mpd) { 29388 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 29389 var _options$manifestUri = options.manifestUri, 29390 manifestUri = _options$manifestUri === undefined ? '' : _options$manifestUri, 29391 _options$NOW = options.NOW, 29392 NOW = _options$NOW === undefined ? Date.now() : _options$NOW, 29393 _options$clientOffset = options.clientOffset, 29394 clientOffset = _options$clientOffset === undefined ? 0 : _options$clientOffset; 29395 29396 var periods = findChildren(mpd, 'Period'); 29397 29398 if (periods.length !== 1) { 29399 // TODO add support for multiperiod 29400 throw new Error(errors.INVALID_NUMBER_OF_PERIOD); 29401 } 29402 29403 var mpdAttributes = parseAttributes$1(mpd); 29404 var mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); 29405 29406 mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; 29407 mpdAttributes.NOW = NOW; 29408 mpdAttributes.clientOffset = clientOffset; 29409 29410 return flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))); 29411 }; 29412 29413 var stringToMpdXml = function stringToMpdXml(manifestString) { 29414 if (manifestString === '') { 29415 throw new Error(errors.DASH_EMPTY_MANIFEST); 29416 } 29417 29418 var parser = new window_1.DOMParser(); 29419 var xml = parser.parseFromString(manifestString, 'application/xml'); 29420 var mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; 29421 29422 if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { 29423 throw new Error(errors.DASH_INVALID_XML); 29424 } 29425 29426 return mpd; 29427 }; 29428 29429 /** 29430 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 29431 * 29432 * @param {string} mpd 29433 * XML string of the MPD manifest 29434 * @return {Object|null} 29435 * Attributes of UTCTiming node specified in the manifest. Null if none found 29436 */ 29437 var parseUTCTimingScheme = function parseUTCTimingScheme(mpd) { 29438 var UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; 29439 29440 if (!UTCTimingNode) { 29441 return null; 29442 } 29443 29444 var attributes = parseAttributes$1(UTCTimingNode); 29445 29446 switch (attributes.schemeIdUri) { 29447 case 'urn:mpeg:dash:utc:http-head:2014': 29448 case 'urn:mpeg:dash:utc:http-head:2012': 29449 attributes.method = 'HEAD'; 29450 break; 29451 case 'urn:mpeg:dash:utc:http-xsdate:2014': 29452 case 'urn:mpeg:dash:utc:http-iso:2014': 29453 case 'urn:mpeg:dash:utc:http-xsdate:2012': 29454 case 'urn:mpeg:dash:utc:http-iso:2012': 29455 attributes.method = 'GET'; 29456 break; 29457 case 'urn:mpeg:dash:utc:direct:2014': 29458 case 'urn:mpeg:dash:utc:direct:2012': 29459 attributes.method = 'DIRECT'; 29460 attributes.value = Date.parse(attributes.value); 29461 break; 29462 case 'urn:mpeg:dash:utc:http-ntp:2014': 29463 case 'urn:mpeg:dash:utc:ntp:2014': 29464 case 'urn:mpeg:dash:utc:sntp:2014': 29465 default: 29466 throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); 29467 } 29468 29469 return attributes; 29470 }; 29471 29472 var parse = function parse(manifestString, options) { 29473 return toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options))); 29474 }; 29475 29476 /** 29477 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 29478 * 29479 * @param {string} manifestString 29480 * XML string of the MPD manifest 29481 * @return {Object|null} 29482 * Attributes of UTCTiming node specified in the manifest. Null if none found 29483 */ 29484 var parseUTCTiming = function parseUTCTiming(manifestString) { 29485 return parseUTCTimingScheme(stringToMpdXml(manifestString)); 29486 }; 29487 29488 var toUnsigned = function toUnsigned(value) { 29489 return value >>> 0; 29490 }; 29491 29492 var bin = { 29493 toUnsigned: toUnsigned 29494 }; 29495 var bin_1 = bin.toUnsigned; 29496 29497 var bin$1 = /*#__PURE__*/Object.freeze({ 29498 default: bin, 29499 __moduleExports: bin, 29500 toUnsigned: bin_1 29501 }); 29502 29503 var require$$0$1 = ( bin$1 && bin ) || bin$1; 29504 29505 var toUnsigned$1 = require$$0$1.toUnsigned; 29506 var _findBox, parseType, timescale, startTime; 29507 29508 // Find the data for a box specified by its path 29509 _findBox = function findBox(data, path) { 29510 var results = [], 29511 i, 29512 size, 29513 type, 29514 end, 29515 subresults; 29516 29517 if (!path.length) { 29518 // short-circuit the search for empty paths 29519 return null; 29520 } 29521 29522 for (i = 0; i < data.byteLength;) { 29523 size = toUnsigned$1(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]); 29524 29525 type = parseType(data.subarray(i + 4, i + 8)); 29526 29527 end = size > 1 ? i + size : data.byteLength; 29528 29529 if (type === path[0]) { 29530 if (path.length === 1) { 29531 // this is the end of the path and we've found the box we were 29532 // looking for 29533 results.push(data.subarray(i + 8, end)); 29534 } else { 29535 // recursively search for the next box along the path 29536 subresults = _findBox(data.subarray(i + 8, end), path.slice(1)); 29537 if (subresults.length) { 29538 results = results.concat(subresults); 29539 } 29540 } 29541 } 29542 i = end; 29543 } 29544 29545 // we've finished searching all of data 29546 return results; 29547 }; 29548 29549 /** 29550 * Returns the string representation of an ASCII encoded four byte buffer. 29551 * @param buffer {Uint8Array} a four-byte buffer to translate 29552 * @return {string} the corresponding string 29553 */ 29554 parseType = function parseType(buffer) { 29555 var result = ''; 29556 result += String.fromCharCode(buffer[0]); 29557 result += String.fromCharCode(buffer[1]); 29558 result += String.fromCharCode(buffer[2]); 29559 result += String.fromCharCode(buffer[3]); 29560 return result; 29561 }; 29562 29563 /** 29564 * Parses an MP4 initialization segment and extracts the timescale 29565 * values for any declared tracks. Timescale values indicate the 29566 * number of clock ticks per second to assume for time-based values 29567 * elsewhere in the MP4. 29568 * 29569 * To determine the start time of an MP4, you need two pieces of 29570 * information: the timescale unit and the earliest base media decode 29571 * time. Multiple timescales can be specified within an MP4 but the 29572 * base media decode time is always expressed in the timescale from 29573 * the media header box for the track: 29574 * ``` 29575 * moov > trak > mdia > mdhd.timescale 29576 * ``` 29577 * @param init {Uint8Array} the bytes of the init segment 29578 * @return {object} a hash of track ids to timescale values or null if 29579 * the init segment is malformed. 29580 */ 29581 timescale = function timescale(init) { 29582 var result = {}, 29583 traks = _findBox(init, ['moov', 'trak']); 29584 29585 // mdhd timescale 29586 return traks.reduce(function (result, trak) { 29587 var tkhd, version, index, id, mdhd; 29588 29589 tkhd = _findBox(trak, ['tkhd'])[0]; 29590 if (!tkhd) { 29591 return null; 29592 } 29593 version = tkhd[0]; 29594 index = version === 0 ? 12 : 20; 29595 id = toUnsigned$1(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]); 29596 29597 mdhd = _findBox(trak, ['mdia', 'mdhd'])[0]; 29598 if (!mdhd) { 29599 return null; 29600 } 29601 version = mdhd[0]; 29602 index = version === 0 ? 12 : 20; 29603 result[id] = toUnsigned$1(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 29604 return result; 29605 }, result); 29606 }; 29607 29608 /** 29609 * Determine the base media decode start time, in seconds, for an MP4 29610 * fragment. If multiple fragments are specified, the earliest time is 29611 * returned. 29612 * 29613 * The base media decode time can be parsed from track fragment 29614 * metadata: 29615 * ``` 29616 * moof > traf > tfdt.baseMediaDecodeTime 29617 * ``` 29618 * It requires the timescale value from the mdhd to interpret. 29619 * 29620 * @param timescale {object} a hash of track ids to timescale values. 29621 * @return {number} the earliest base media decode start time for the 29622 * fragment, in seconds 29623 */ 29624 startTime = function startTime(timescale, fragment) { 29625 var trafs, baseTimes, result; 29626 29627 // we need info from two childrend of each track fragment box 29628 trafs = _findBox(fragment, ['moof', 'traf']); 29629 29630 // determine the start times for each track 29631 baseTimes = [].concat.apply([], trafs.map(function (traf) { 29632 return _findBox(traf, ['tfhd']).map(function (tfhd) { 29633 var id, scale, baseTime; 29634 29635 // get the track id from the tfhd 29636 id = toUnsigned$1(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); 29637 // assume a 90kHz clock if no timescale was specified 29638 scale = timescale[id] || 90e3; 29639 29640 // get the base media decode time from the tfdt 29641 baseTime = _findBox(traf, ['tfdt']).map(function (tfdt) { 29642 var version, result; 29643 29644 version = tfdt[0]; 29645 result = toUnsigned$1(tfdt[4] << 24 | tfdt[5] << 16 | tfdt[6] << 8 | tfdt[7]); 29646 if (version === 1) { 29647 result *= Math.pow(2, 32); 29648 result += toUnsigned$1(tfdt[8] << 24 | tfdt[9] << 16 | tfdt[10] << 8 | tfdt[11]); 29649 } 29650 return result; 29651 })[0]; 29652 baseTime = baseTime || Infinity; 29653 29654 // convert base time to seconds 29655 return baseTime / scale; 29656 }); 29657 })); 29658 29659 // return the minimum 29660 result = Math.min.apply(null, baseTimes); 29661 return isFinite(result) ? result : 0; 29662 }; 29663 29664 var probe = { 29665 parseType: parseType, 29666 timescale: timescale, 29667 startTime: startTime 29668 }; 29669 29670 var streamTypes = { 29671 H264_STREAM_TYPE: 0x1B, 29672 ADTS_STREAM_TYPE: 0x0F, 29673 METADATA_STREAM_TYPE: 0x15 29674 }; 29675 var streamTypes_1 = streamTypes.H264_STREAM_TYPE; 29676 var streamTypes_2 = streamTypes.ADTS_STREAM_TYPE; 29677 var streamTypes_3 = streamTypes.METADATA_STREAM_TYPE; 29678 29679 var streamTypes$1 = /*#__PURE__*/Object.freeze({ 29680 default: streamTypes, 29681 __moduleExports: streamTypes, 29682 H264_STREAM_TYPE: streamTypes_1, 29683 ADTS_STREAM_TYPE: streamTypes_2, 29684 METADATA_STREAM_TYPE: streamTypes_3 29685 }); 29686 29687 /** 29688 * mux.js 29689 * 29690 * Copyright (c) 2014 Brightcove 29691 * All rights reserved. 29692 * 29693 * A lightweight readable stream implemention that handles event dispatching. 29694 * Objects that inherit from streams should call init in their constructors. 29695 */ 29696 29697 var Stream$1 = function Stream() { 29698 this.init = function () { 29699 var listeners = {}; 29700 /** 29701 * Add a listener for a specified event type. 29702 * @param type {string} the event name 29703 * @param listener {function} the callback to be invoked when an event of 29704 * the specified type occurs 29705 */ 29706 this.on = function (type, listener) { 29707 if (!listeners[type]) { 29708 listeners[type] = []; 29709 } 29710 listeners[type] = listeners[type].concat(listener); 29711 }; 29712 /** 29713 * Remove a listener for a specified event type. 29714 * @param type {string} the event name 29715 * @param listener {function} a function previously registered for this 29716 * type of event through `on` 29717 */ 29718 this.off = function (type, listener) { 29719 var index; 29720 if (!listeners[type]) { 29721 return false;
vendor: 20,700 bytes, lines 29722-30341
29722 } 29723 index = listeners[type].indexOf(listener); 29724 listeners[type] = listeners[type].slice(); 29725 listeners[type].splice(index, 1); 29726 return index > -1; 29727 }; 29728 /** 29729 * Trigger an event of the specified type on this stream. Any additional 29730 * arguments to this function are passed as parameters to event listeners. 29731 * @param type {string} the event name 29732 */ 29733 this.trigger = function (type) { 29734 var callbacks, i, length, args; 29735 callbacks = listeners[type]; 29736 if (!callbacks) { 29737 return; 29738 } 29739 // Slicing the arguments on every invocation of this method 29740 // can add a significant amount of overhead. Avoid the 29741 // intermediate object creation for the common case of a 29742 // single callback argument 29743 if (arguments.length === 2) { 29744 length = callbacks.length; 29745 for (i = 0; i < length; ++i) { 29746 callbacks[i].call(this, arguments[1]); 29747 } 29748 } else { 29749 args = []; 29750 i = arguments.length; 29751 for (i = 1; i < arguments.length; ++i) { 29752 args.push(arguments[i]); 29753 } 29754 length = callbacks.length; 29755 for (i = 0; i < length; ++i) { 29756 callbacks[i].apply(this, args); 29757 } 29758 } 29759 }; 29760 /** 29761 * Destroys the stream and cleans up. 29762 */ 29763 this.dispose = function () { 29764 listeners = {}; 29765 }; 29766 }; 29767 }; 29768 29769 /** 29770 * Forwards all `data` events on this stream to the destination stream. The 29771 * destination stream should provide a method `push` to receive the data 29772 * events as they arrive. 29773 * @param destination {stream} the stream that will receive all `data` events 29774 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 29775 * when the current stream emits a 'done' event 29776 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 29777 */ 29778 Stream$1.prototype.pipe = function (destination) { 29779 this.on('data', function (data) { 29780 destination.push(data); 29781 }); 29782 29783 this.on('done', function (flushSource) { 29784 destination.flush(flushSource); 29785 }); 29786 29787 return destination; 29788 }; 29789 29790 // Default stream functions that are expected to be overridden to perform 29791 // actual work. These are provided by the prototype as a sort of no-op 29792 // implementation so that we don't have to check for their existence in the 29793 // `pipe` function above. 29794 Stream$1.prototype.push = function (data) { 29795 this.trigger('data', data); 29796 }; 29797 29798 Stream$1.prototype.flush = function (flushSource) { 29799 this.trigger('done', flushSource); 29800 }; 29801 29802 var stream = Stream$1; 29803 29804 var stream$1 = /*#__PURE__*/Object.freeze({ 29805 default: stream, 29806 __moduleExports: stream 29807 }); 29808 29809 var Stream$2 = ( stream$1 && stream ) || stream$1; 29810 29811 var MAX_TS = 8589934592; 29812 29813 var RO_THRESH = 4294967296; 29814 29815 var handleRollover = function handleRollover(value, reference) { 29816 var direction = 1; 29817 29818 if (value > reference) { 29819 // If the current timestamp value is greater than our reference timestamp and we detect a 29820 // timestamp rollover, this means the roll over is happening in the opposite direction. 29821 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 29822 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 29823 // rollover point. In loading this segment, the timestamp values will be very large, 29824 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 29825 // the time stamp to be `value - 2^33`. 29826 direction = -1; 29827 } 29828 29829 // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 29830 // cause an incorrect adjustment. 29831 while (Math.abs(reference - value) > RO_THRESH) { 29832 value += direction * MAX_TS; 29833 } 29834 29835 return value; 29836 }; 29837 29838 var TimestampRolloverStream = function TimestampRolloverStream(type) { 29839 var lastDTS, referenceDTS; 29840 29841 TimestampRolloverStream.prototype.init.call(this); 29842 29843 this.type_ = type; 29844 29845 this.push = function (data) { 29846 if (data.type !== this.type_) { 29847 return; 29848 } 29849 29850 if (referenceDTS === undefined) { 29851 referenceDTS = data.dts; 29852 } 29853 29854 data.dts = handleRollover(data.dts, referenceDTS); 29855 data.pts = handleRollover(data.pts, referenceDTS); 29856 29857 lastDTS = data.dts; 29858 29859 this.trigger('data', data); 29860 }; 29861 29862 this.flush = function () { 29863 referenceDTS = lastDTS; 29864 this.trigger('done'); 29865 }; 29866 29867 this.discontinuity = function () { 29868 referenceDTS = void 0; 29869 lastDTS = void 0; 29870 }; 29871 }; 29872 29873 TimestampRolloverStream.prototype = new Stream$2(); 29874 29875 var timestampRolloverStream = { 29876 TimestampRolloverStream: TimestampRolloverStream, 29877 handleRollover: handleRollover 29878 }; 29879 var timestampRolloverStream_1 = timestampRolloverStream.TimestampRolloverStream; 29880 var timestampRolloverStream_2 = timestampRolloverStream.handleRollover; 29881 29882 var timestampRolloverStream$1 = /*#__PURE__*/Object.freeze({ 29883 default: timestampRolloverStream, 29884 __moduleExports: timestampRolloverStream, 29885 TimestampRolloverStream: timestampRolloverStream_1, 29886 handleRollover: timestampRolloverStream_2 29887 }); 29888 29889 var StreamTypes = ( streamTypes$1 && streamTypes ) || streamTypes$1; 29890 29891 var parsePid = function parsePid(packet) { 29892 var pid = packet[1] & 0x1f; 29893 pid <<= 8; 29894 pid |= packet[2]; 29895 return pid; 29896 }; 29897 29898 var parsePayloadUnitStartIndicator = function parsePayloadUnitStartIndicator(packet) { 29899 return !!(packet[1] & 0x40); 29900 }; 29901 29902 var parseAdaptionField = function parseAdaptionField(packet) { 29903 var offset = 0; 29904 // if an adaption field is present, its length is specified by the 29905 // fifth byte of the TS packet header. The adaptation field is 29906 // used to add stuffing to PES packets that don't fill a complete 29907 // TS packet, and to specify some forms of timing and control data 29908 // that we do not currently use. 29909 if ((packet[3] & 0x30) >>> 4 > 0x01) { 29910 offset += packet[4] + 1; 29911 } 29912 return offset; 29913 }; 29914 29915 var parseType$1 = function parseType(packet, pmtPid) { 29916 var pid = parsePid(packet); 29917 if (pid === 0) { 29918 return 'pat'; 29919 } else if (pid === pmtPid) { 29920 return 'pmt'; 29921 } else if (pmtPid) { 29922 return 'pes'; 29923 } 29924 return null; 29925 }; 29926 29927 var parsePat = function parsePat(packet) { 29928 var pusi = parsePayloadUnitStartIndicator(packet); 29929 var offset = 4 + parseAdaptionField(packet); 29930 29931 if (pusi) { 29932 offset += packet[offset] + 1; 29933 } 29934 29935 return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; 29936 }; 29937 29938 var parsePmt = function parsePmt(packet) { 29939 var programMapTable = {}; 29940 var pusi = parsePayloadUnitStartIndicator(packet); 29941 var payloadOffset = 4 + parseAdaptionField(packet); 29942 29943 if (pusi) { 29944 payloadOffset += packet[payloadOffset] + 1; 29945 } 29946 29947 // PMTs can be sent ahead of the time when they should actually 29948 // take effect. We don't believe this should ever be the case 29949 // for HLS but we'll ignore "forward" PMT declarations if we see 29950 // them. Future PMT declarations have the current_next_indicator 29951 // set to zero. 29952 if (!(packet[payloadOffset + 5] & 0x01)) { 29953 return; 29954 } 29955 29956 var sectionLength, tableEnd, programInfoLength; 29957 // the mapping table ends at the end of the current section 29958 sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; 29959 tableEnd = 3 + sectionLength - 4; 29960 29961 // to determine where the table is, we have to figure out how 29962 // long the program info descriptors are 29963 programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; 29964 29965 // advance the offset to the first entry in the mapping table 29966 var offset = 12 + programInfoLength; 29967 while (offset < tableEnd) { 29968 var i = payloadOffset + offset; 29969 // add an entry that maps the elementary_pid to the stream_type 29970 programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; 29971 29972 // move to the next table entry 29973 // skip past the elementary stream descriptors, if present 29974 offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; 29975 } 29976 return programMapTable; 29977 }; 29978 29979 var parsePesType = function parsePesType(packet, programMapTable) { 29980 var pid = parsePid(packet); 29981 var type = programMapTable[pid]; 29982 switch (type) { 29983 case StreamTypes.H264_STREAM_TYPE: 29984 return 'video'; 29985 case StreamTypes.ADTS_STREAM_TYPE: 29986 return 'audio'; 29987 case StreamTypes.METADATA_STREAM_TYPE: 29988 return 'timed-metadata'; 29989 default: 29990 return null; 29991 } 29992 }; 29993 29994 var parsePesTime = function parsePesTime(packet) { 29995 var pusi = parsePayloadUnitStartIndicator(packet); 29996 if (!pusi) { 29997 return null; 29998 } 29999 30000 var offset = 4 + parseAdaptionField(packet); 30001 30002 if (offset >= packet.byteLength) { 30003 // From the H 222.0 MPEG-TS spec 30004 // "For transport stream packets carrying PES packets, stuffing is needed when there 30005 // is insufficient PES packet data to completely fill the transport stream packet 30006 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 30007 // the sum of the lengths of the data elements in it, so that the payload bytes 30008 // remaining after the adaptation field exactly accommodates the available PES packet 30009 // data." 30010 // 30011 // If the offset is >= the length of the packet, then the packet contains no data 30012 // and instead is just adaption field stuffing bytes 30013 return null; 30014 } 30015 30016 var pes = null; 30017 var ptsDtsFlags; 30018 30019 // PES packets may be annotated with a PTS value, or a PTS value 30020 // and a DTS value. Determine what combination of values is 30021 // available to work with. 30022 ptsDtsFlags = packet[offset + 7]; 30023 30024 // PTS and DTS are normally stored as a 33-bit number. Javascript 30025 // performs all bitwise operations on 32-bit integers but javascript 30026 // supports a much greater range (52-bits) of integer using standard 30027 // mathematical operations. 30028 // We construct a 31-bit value using bitwise operators over the 31 30029 // most significant bits and then multiply by 4 (equal to a left-shift 30030 // of 2) before we add the final 2 least significant bits of the 30031 // timestamp (equal to an OR.) 30032 if (ptsDtsFlags & 0xC0) { 30033 pes = {}; 30034 // the PTS and DTS are not written out directly. For information 30035 // on how they are encoded, see 30036 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 30037 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; 30038 pes.pts *= 4; // Left shift by 2 30039 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 30040 pes.dts = pes.pts; 30041 if (ptsDtsFlags & 0x40) { 30042 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; 30043 pes.dts *= 4; // Left shift by 2 30044 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 30045 } 30046 } 30047 return pes; 30048 }; 30049 30050 var parseNalUnitType = function parseNalUnitType(type) { 30051 switch (type) { 30052 case 0x05: 30053 return 'slice_layer_without_partitioning_rbsp_idr'; 30054 case 0x06: 30055 return 'sei_rbsp'; 30056 case 0x07: 30057 return 'seq_parameter_set_rbsp'; 30058 case 0x08: 30059 return 'pic_parameter_set_rbsp'; 30060 case 0x09: 30061 return 'access_unit_delimiter_rbsp'; 30062 default: 30063 return null; 30064 } 30065 }; 30066 30067 var videoPacketContainsKeyFrame = function videoPacketContainsKeyFrame(packet) { 30068 var offset = 4 + parseAdaptionField(packet); 30069 var frameBuffer = packet.subarray(offset); 30070 var frameI = 0; 30071 var frameSyncPoint = 0; 30072 var foundKeyFrame = false; 30073 var nalType; 30074 30075 // advance the sync point to a NAL start, if necessary 30076 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 30077 if (frameBuffer[frameSyncPoint + 2] === 1) { 30078 // the sync point is properly aligned 30079 frameI = frameSyncPoint + 5; 30080 break; 30081 } 30082 } 30083 30084 while (frameI < frameBuffer.byteLength) { 30085 // look at the current byte to determine if we've hit the end of 30086 // a NAL unit boundary 30087 switch (frameBuffer[frameI]) { 30088 case 0: 30089 // skip past non-sync sequences 30090 if (frameBuffer[frameI - 1] !== 0) { 30091 frameI += 2; 30092 break; 30093 } else if (frameBuffer[frameI - 2] !== 0) { 30094 frameI++; 30095 break; 30096 } 30097 30098 if (frameSyncPoint + 3 !== frameI - 2) { 30099 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 30100 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 30101 foundKeyFrame = true; 30102 } 30103 } 30104 30105 // drop trailing zeroes 30106 do { 30107 frameI++; 30108 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 30109 frameSyncPoint = frameI - 2; 30110 frameI += 3; 30111 break; 30112 case 1: 30113 // skip past non-sync sequences 30114 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 30115 frameI += 3; 30116 break; 30117 } 30118 30119 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 30120 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 30121 foundKeyFrame = true; 30122 } 30123 frameSyncPoint = frameI - 2; 30124 frameI += 3; 30125 break; 30126 default: 30127 // the current byte isn't a one or zero, so it cannot be part 30128 // of a sync sequence 30129 frameI += 3; 30130 break; 30131 } 30132 } 30133 frameBuffer = frameBuffer.subarray(frameSyncPoint); 30134 frameI -= frameSyncPoint; 30135 frameSyncPoint = 0; 30136 // parse the final nal 30137 if (frameBuffer && frameBuffer.byteLength > 3) { 30138 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 30139 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 30140 foundKeyFrame = true; 30141 } 30142 } 30143 30144 return foundKeyFrame; 30145 }; 30146 30147 var probe$1 = { 30148 parseType: parseType$1, 30149 parsePat: parsePat, 30150 parsePmt: parsePmt, 30151 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 30152 parsePesType: parsePesType, 30153 parsePesTime: parsePesTime, 30154 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame 30155 }; 30156 var probe_1$1 = probe$1.parseType; 30157 var probe_2$1 = probe$1.parsePat; 30158 var probe_3$1 = probe$1.parsePmt; 30159 var probe_4 = probe$1.parsePayloadUnitStartIndicator; 30160 var probe_5 = probe$1.parsePesType; 30161 var probe_6 = probe$1.parsePesTime; 30162 var probe_7 = probe$1.videoPacketContainsKeyFrame; 30163 30164 var probe$2 = /*#__PURE__*/Object.freeze({ 30165 default: probe$1, 30166 __moduleExports: probe$1, 30167 parseType: probe_1$1, 30168 parsePat: probe_2$1, 30169 parsePmt: probe_3$1, 30170 parsePayloadUnitStartIndicator: probe_4, 30171 parsePesType: probe_5, 30172 parsePesTime: probe_6, 30173 videoPacketContainsKeyFrame: probe_7 30174 }); 30175 30176 /** 30177 * mux.js 30178 * 30179 * Copyright (c) 2016 Brightcove 30180 * All rights reserved. 30181 * 30182 * Utilities to detect basic properties and metadata about Aac data. 30183 */ 30184 30185 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 30186 30187 var parseSyncSafeInteger = function parseSyncSafeInteger(data) { 30188 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 30189 }; 30190 30191 // return a percent-encoded representation of the specified byte range 30192 // @see http://en.wikipedia.org/wiki/Percent-encoding 30193 var percentEncode = function percentEncode(bytes, start, end) { 30194 var i, 30195 result = ''; 30196 for (i = start; i < end; i++) { 30197 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 30198 } 30199 return result; 30200 }; 30201 30202 // return the string representation of the specified byte range, 30203 // interpreted as ISO-8859-1. 30204 var parseIso88591 = function parseIso88591(bytes, start, end) { 30205 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 30206 }; 30207 30208 var parseId3TagSize = function parseId3TagSize(header, byteIndex) { 30209 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 30210 flags = header[byteIndex + 5], 30211 footerPresent = (flags & 16) >> 4; 30212 30213 if (footerPresent) { 30214 return returnSize + 20; 30215 } 30216 return returnSize + 10; 30217 }; 30218 30219 var parseAdtsSize = function parseAdtsSize(header, byteIndex) { 30220 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 30221 middle = header[byteIndex + 4] << 3, 30222 highTwo = header[byteIndex + 3] & 0x3 << 11; 30223 30224 return highTwo | middle | lowThree; 30225 }; 30226 30227 var parseType$2 = function parseType(header, byteIndex) { 30228 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 30229 return 'timed-metadata'; 30230 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 30231 return 'audio'; 30232 } 30233 return null; 30234 }; 30235 30236 var parseSampleRate = function parseSampleRate(packet) { 30237 var i = 0; 30238 30239 while (i + 5 < packet.length) { 30240 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 30241 // If a valid header was not found, jump one forward and attempt to 30242 // find a valid ADTS header starting at the next byte 30243 i++; 30244 continue; 30245 } 30246 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 30247 } 30248 30249 return null; 30250 }; 30251 30252 var parseAacTimestamp = function parseAacTimestamp(packet) { 30253 var frameStart, frameSize, frame, frameHeader; 30254 30255 // find the start of the first frame and the end of the tag 30256 frameStart = 10; 30257 if (packet[5] & 0x40) { 30258 // advance the frame start past the extended header 30259 frameStart += 4; // header size field 30260 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 30261 } 30262 30263 // parse one or more ID3 frames 30264 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 30265 do { 30266 // determine the number of bytes in this frame 30267 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 30268 if (frameSize < 1) { 30269 return null; 30270 } 30271 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 30272 30273 if (frameHeader === 'PRIV') { 30274 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 30275 30276 for (var i = 0; i < frame.byteLength; i++) { 30277 if (frame[i] === 0) { 30278 var owner = parseIso88591(frame, 0, i); 30279 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 30280 var d = frame.subarray(i + 1); 30281 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 30282 size *= 4; 30283 size += d[7] & 0x03; 30284 30285 return size; 30286 } 30287 break; 30288 } 30289 } 30290 } 30291 30292 frameStart += 10; // advance past the frame header 30293 frameStart += frameSize; // advance past the frame body 30294 } while (frameStart < packet.byteLength); 30295 return null; 30296 }; 30297 30298 var probe$3 = { 30299 parseId3TagSize: parseId3TagSize, 30300 parseAdtsSize: parseAdtsSize, 30301 parseType: parseType$2, 30302 parseSampleRate: parseSampleRate, 30303 parseAacTimestamp: parseAacTimestamp 30304 }; 30305 var probe_1$2 = probe$3.parseId3TagSize; 30306 var probe_2$2 = probe$3.parseAdtsSize; 30307 var probe_3$2 = probe$3.parseType; 30308 var probe_4$1 = probe$3.parseSampleRate; 30309 var probe_5$1 = probe$3.parseAacTimestamp; 30310 30311 var probe$4 = /*#__PURE__*/Object.freeze({ 30312 default: probe$3, 30313 __moduleExports: probe$3, 30314 parseId3TagSize: probe_1$2, 30315 parseAdtsSize: probe_2$2, 30316 parseType: probe_3$2, 30317 parseSampleRate: probe_4$1, 30318 parseAacTimestamp: probe_5$1 30319 }); 30320 30321 var require$$0$2 = ( timestampRolloverStream$1 && timestampRolloverStream ) || timestampRolloverStream$1; 30322 30323 var require$$1$1 = ( probe$2 && probe$1 ) || probe$2; 30324 30325 var require$$2$1 = ( probe$4 && probe$3 ) || probe$4; 30326 30327 var handleRollover$1 = require$$0$2.handleRollover; 30328 var probe$5 = {}; 30329 probe$5.ts = require$$1$1; 30330 probe$5.aac = require$$2$1; 30331 30332 var PES_TIMESCALE = 90000, 30333 MP2T_PACKET_LENGTH = 188, 30334 // bytes 30335 SYNC_BYTE = 0x47; 30336 30337 var isLikelyAacData = function isLikelyAacData(data) { 30338 if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) { 30339 return true; 30340 } 30341 return false;
vendor: 9,174 bytes, lines 30342-30636
30342 }; 30343 30344 /** 30345 * walks through segment data looking for pat and pmt packets to parse out 30346 * program map table information 30347 */ 30348 var parsePsi_ = function parsePsi_(bytes, pmt) { 30349 var startIndex = 0, 30350 endIndex = MP2T_PACKET_LENGTH, 30351 packet, 30352 type; 30353 30354 while (endIndex < bytes.byteLength) { 30355 // Look for a pair of start and end sync bytes in the data.. 30356 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30357 // We found a packet 30358 packet = bytes.subarray(startIndex, endIndex); 30359 type = probe$5.ts.parseType(packet, pmt.pid); 30360 30361 switch (type) { 30362 case 'pat': 30363 if (!pmt.pid) { 30364 pmt.pid = probe$5.ts.parsePat(packet); 30365 } 30366 break; 30367 case 'pmt': 30368 if (!pmt.table) { 30369 pmt.table = probe$5.ts.parsePmt(packet); 30370 } 30371 break; 30372 default: 30373 break; 30374 } 30375 30376 // Found the pat and pmt, we can stop walking the segment 30377 if (pmt.pid && pmt.table) { 30378 return; 30379 } 30380 30381 startIndex += MP2T_PACKET_LENGTH; 30382 endIndex += MP2T_PACKET_LENGTH; 30383 continue; 30384 } 30385 30386 // If we get here, we have somehow become de-synchronized and we need to step 30387 // forward one byte at a time until we find a pair of sync bytes that denote 30388 // a packet 30389 startIndex++; 30390 endIndex++; 30391 } 30392 }; 30393 30394 /** 30395 * walks through the segment data from the start and end to get timing information 30396 * for the first and last audio pes packets 30397 */ 30398 var parseAudioPes_ = function parseAudioPes_(bytes, pmt, result) { 30399 var startIndex = 0, 30400 endIndex = MP2T_PACKET_LENGTH, 30401 packet, 30402 type, 30403 pesType, 30404 pusi, 30405 parsed; 30406 30407 var endLoop = false; 30408 30409 // Start walking from start of segment to get first audio packet 30410 while (endIndex < bytes.byteLength) { 30411 // Look for a pair of start and end sync bytes in the data.. 30412 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30413 // We found a packet 30414 packet = bytes.subarray(startIndex, endIndex); 30415 type = probe$5.ts.parseType(packet, pmt.pid); 30416 30417 switch (type) { 30418 case 'pes': 30419 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30420 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30421 if (pesType === 'audio' && pusi) { 30422 parsed = probe$5.ts.parsePesTime(packet); 30423 if (parsed) { 30424 parsed.type = 'audio'; 30425 result.audio.push(parsed); 30426 endLoop = true; 30427 } 30428 } 30429 break; 30430 default: 30431 break; 30432 } 30433 30434 if (endLoop) { 30435 break; 30436 } 30437 30438 startIndex += MP2T_PACKET_LENGTH; 30439 endIndex += MP2T_PACKET_LENGTH; 30440 continue; 30441 } 30442 30443 // If we get here, we have somehow become de-synchronized and we need to step 30444 // forward one byte at a time until we find a pair of sync bytes that denote 30445 // a packet 30446 startIndex++; 30447 endIndex++; 30448 } 30449 30450 // Start walking from end of segment to get last audio packet 30451 endIndex = bytes.byteLength; 30452 startIndex = endIndex - MP2T_PACKET_LENGTH; 30453 endLoop = false; 30454 while (startIndex >= 0) { 30455 // Look for a pair of start and end sync bytes in the data.. 30456 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30457 // We found a packet 30458 packet = bytes.subarray(startIndex, endIndex); 30459 type = probe$5.ts.parseType(packet, pmt.pid); 30460 30461 switch (type) { 30462 case 'pes': 30463 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30464 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30465 if (pesType === 'audio' && pusi) { 30466 parsed = probe$5.ts.parsePesTime(packet); 30467 if (parsed) { 30468 parsed.type = 'audio'; 30469 result.audio.push(parsed); 30470 endLoop = true; 30471 } 30472 } 30473 break; 30474 default: 30475 break; 30476 } 30477 30478 if (endLoop) { 30479 break; 30480 } 30481 30482 startIndex -= MP2T_PACKET_LENGTH; 30483 endIndex -= MP2T_PACKET_LENGTH; 30484 continue; 30485 } 30486 30487 // If we get here, we have somehow become de-synchronized and we need to step 30488 // forward one byte at a time until we find a pair of sync bytes that denote 30489 // a packet 30490 startIndex--; 30491 endIndex--; 30492 } 30493 }; 30494 30495 /** 30496 * walks through the segment data from the start and end to get timing information 30497 * for the first and last video pes packets as well as timing information for the first 30498 * key frame. 30499 */ 30500 var parseVideoPes_ = function parseVideoPes_(bytes, pmt, result) { 30501 var startIndex = 0, 30502 endIndex = MP2T_PACKET_LENGTH, 30503 packet, 30504 type, 30505 pesType, 30506 pusi, 30507 parsed, 30508 frame, 30509 i, 30510 pes; 30511 30512 var endLoop = false; 30513 30514 var currentFrame = { 30515 data: [], 30516 size: 0 30517 }; 30518 30519 // Start walking from start of segment to get first video packet 30520 while (endIndex < bytes.byteLength) { 30521 // Look for a pair of start and end sync bytes in the data.. 30522 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30523 // We found a packet 30524 packet = bytes.subarray(startIndex, endIndex); 30525 type = probe$5.ts.parseType(packet, pmt.pid); 30526 30527 switch (type) { 30528 case 'pes': 30529 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30530 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30531 if (pesType === 'video') { 30532 if (pusi && !endLoop) { 30533 parsed = probe$5.ts.parsePesTime(packet); 30534 if (parsed) { 30535 parsed.type = 'video'; 30536 result.video.push(parsed); 30537 endLoop = true; 30538 } 30539 } 30540 if (!result.firstKeyFrame) { 30541 if (pusi) { 30542 if (currentFrame.size !== 0) { 30543 frame = new Uint8Array(currentFrame.size); 30544 i = 0; 30545 while (currentFrame.data.length) { 30546 pes = currentFrame.data.shift(); 30547 frame.set(pes, i); 30548 i += pes.byteLength; 30549 } 30550 if (probe$5.ts.videoPacketContainsKeyFrame(frame)) { 30551 result.firstKeyFrame = probe$5.ts.parsePesTime(frame); 30552 result.firstKeyFrame.type = 'video'; 30553 } 30554 currentFrame.size = 0; 30555 } 30556 } 30557 currentFrame.data.push(packet); 30558 currentFrame.size += packet.byteLength; 30559 } 30560 } 30561 break; 30562 default: 30563 break; 30564 } 30565 30566 if (endLoop && result.firstKeyFrame) { 30567 break; 30568 } 30569 30570 startIndex += MP2T_PACKET_LENGTH; 30571 endIndex += MP2T_PACKET_LENGTH; 30572 continue; 30573 } 30574 30575 // If we get here, we have somehow become de-synchronized and we need to step 30576 // forward one byte at a time until we find a pair of sync bytes that denote 30577 // a packet 30578 startIndex++; 30579 endIndex++; 30580 } 30581 30582 // Start walking from end of segment to get last video packet 30583 endIndex = bytes.byteLength; 30584 startIndex = endIndex - MP2T_PACKET_LENGTH; 30585 endLoop = false; 30586 while (startIndex >= 0) { 30587 // Look for a pair of start and end sync bytes in the data.. 30588 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30589 // We found a packet 30590 packet = bytes.subarray(startIndex, endIndex); 30591 type = probe$5.ts.parseType(packet, pmt.pid); 30592 30593 switch (type) { 30594 case 'pes': 30595 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30596 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30597 if (pesType === 'video' && pusi) { 30598 parsed = probe$5.ts.parsePesTime(packet); 30599 if (parsed) { 30600 parsed.type = 'video'; 30601 result.video.push(parsed); 30602 endLoop = true; 30603 } 30604 } 30605 break; 30606 default: 30607 break; 30608 } 30609 30610 if (endLoop) { 30611 break; 30612 } 30613 30614 startIndex -= MP2T_PACKET_LENGTH; 30615 endIndex -= MP2T_PACKET_LENGTH; 30616 continue; 30617 } 30618 30619 // If we get here, we have somehow become de-synchronized and we need to step 30620 // forward one byte at a time until we find a pair of sync bytes that denote 30621 // a packet 30622 startIndex--; 30623 endIndex--; 30624 } 30625 }; 30626 30627 /** 30628 * Adjusts the timestamp information for the segment to account for 30629 * rollover and convert to seconds based on pes packet timescale (90khz clock) 30630 */ 30631 var adjustTimestamp_ = function adjustTimestamp_(segmentInfo, baseTimestamp) { 30632 if (segmentInfo.audio && segmentInfo.audio.length) { 30633 var audioBaseTimestamp = baseTimestamp; 30634 if (typeof audioBaseTimestamp === 'undefined') { 30635 audioBaseTimestamp = segmentInfo.audio[0].dts; 30636 }
vendor: 5,675 bytes, lines 30637-30835
30637 segmentInfo.audio.forEach(function (info) { 30638 info.dts = handleRollover$1(info.dts, audioBaseTimestamp); 30639 info.pts = handleRollover$1(info.pts, audioBaseTimestamp); 30640 // time in seconds 30641 info.dtsTime = info.dts / PES_TIMESCALE; 30642 info.ptsTime = info.pts / PES_TIMESCALE; 30643 }); 30644 } 30645 30646 if (segmentInfo.video && segmentInfo.video.length) { 30647 var videoBaseTimestamp = baseTimestamp; 30648 if (typeof videoBaseTimestamp === 'undefined') { 30649 videoBaseTimestamp = segmentInfo.video[0].dts; 30650 } 30651 segmentInfo.video.forEach(function (info) { 30652 info.dts = handleRollover$1(info.dts, videoBaseTimestamp); 30653 info.pts = handleRollover$1(info.pts, videoBaseTimestamp); 30654 // time in seconds 30655 info.dtsTime = info.dts / PES_TIMESCALE; 30656 info.ptsTime = info.pts / PES_TIMESCALE; 30657 }); 30658 if (segmentInfo.firstKeyFrame) { 30659 var frame = segmentInfo.firstKeyFrame; 30660 frame.dts = handleRollover$1(frame.dts, videoBaseTimestamp); 30661 frame.pts = handleRollover$1(frame.pts, videoBaseTimestamp); 30662 // time in seconds 30663 frame.dtsTime = frame.dts / PES_TIMESCALE; 30664 frame.ptsTime = frame.dts / PES_TIMESCALE; 30665 } 30666 } 30667 }; 30668 30669 /** 30670 * inspects the aac data stream for start and end time information 30671 */ 30672 var inspectAac_ = function inspectAac_(bytes) { 30673 var endLoop = false, 30674 audioCount = 0, 30675 sampleRate = null, 30676 timestamp = null, 30677 frameSize = 0, 30678 byteIndex = 0, 30679 packet; 30680 30681 while (bytes.length - byteIndex >= 3) { 30682 var type = probe$5.aac.parseType(bytes, byteIndex); 30683 switch (type) { 30684 case 'timed-metadata': 30685 // Exit early because we don't have enough to parse 30686 // the ID3 tag header 30687 if (bytes.length - byteIndex < 10) { 30688 endLoop = true; 30689 break; 30690 } 30691 30692 frameSize = probe$5.aac.parseId3TagSize(bytes, byteIndex); 30693 30694 // Exit early if we don't have enough in the buffer 30695 // to emit a full packet 30696 if (frameSize > bytes.length) { 30697 endLoop = true; 30698 break; 30699 } 30700 if (timestamp === null) { 30701 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 30702 timestamp = probe$5.aac.parseAacTimestamp(packet); 30703 } 30704 byteIndex += frameSize; 30705 break; 30706 case 'audio': 30707 // Exit early because we don't have enough to parse 30708 // the ADTS frame header 30709 if (bytes.length - byteIndex < 7) { 30710 endLoop = true; 30711 break; 30712 } 30713 30714 frameSize = probe$5.aac.parseAdtsSize(bytes, byteIndex); 30715 30716 // Exit early if we don't have enough in the buffer 30717 // to emit a full packet 30718 if (frameSize > bytes.length) { 30719 endLoop = true; 30720 break; 30721 } 30722 if (sampleRate === null) { 30723 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 30724 sampleRate = probe$5.aac.parseSampleRate(packet); 30725 } 30726 audioCount++; 30727 byteIndex += frameSize; 30728 break; 30729 default: 30730 byteIndex++; 30731 break; 30732 } 30733 if (endLoop) { 30734 return null; 30735 } 30736 } 30737 if (sampleRate === null || timestamp === null) { 30738 return null; 30739 } 30740 30741 var audioTimescale = PES_TIMESCALE / sampleRate; 30742 30743 var result = { 30744 audio: [{ 30745 type: 'audio', 30746 dts: timestamp, 30747 pts: timestamp 30748 }, { 30749 type: 'audio', 30750 dts: timestamp + audioCount * 1024 * audioTimescale, 30751 pts: timestamp + audioCount * 1024 * audioTimescale 30752 }] 30753 }; 30754 30755 return result; 30756 }; 30757 30758 /** 30759 * inspects the transport stream segment data for start and end time information 30760 * of the audio and video tracks (when present) as well as the first key frame's 30761 * start time. 30762 */ 30763 var inspectTs_ = function inspectTs_(bytes) { 30764 var pmt = { 30765 pid: null, 30766 table: null 30767 }; 30768 30769 var result = {}; 30770 30771 parsePsi_(bytes, pmt); 30772 30773 for (var pid in pmt.table) { 30774 if (pmt.table.hasOwnProperty(pid)) { 30775 var type = pmt.table[pid]; 30776 switch (type) { 30777 case StreamTypes.H264_STREAM_TYPE: 30778 result.video = []; 30779 parseVideoPes_(bytes, pmt, result); 30780 if (result.video.length === 0) { 30781 delete result.video; 30782 } 30783 break; 30784 case StreamTypes.ADTS_STREAM_TYPE: 30785 result.audio = []; 30786 parseAudioPes_(bytes, pmt, result); 30787 if (result.audio.length === 0) { 30788 delete result.audio; 30789 } 30790 break; 30791 default: 30792 break; 30793 } 30794 } 30795 } 30796 return result; 30797 }; 30798 30799 /** 30800 * Inspects segment byte data and returns an object with start and end timing information 30801 * 30802 * @param {Uint8Array} bytes The segment byte data 30803 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 30804 * timestamps for rollover. This value must be in 90khz clock. 30805 * @return {Object} Object containing start and end frame timing info of segment. 30806 */ 30807 var inspect = function inspect(bytes, baseTimestamp) { 30808 var isAacData = isLikelyAacData(bytes); 30809 30810 var result; 30811 30812 if (isAacData) { 30813 result = inspectAac_(bytes); 30814 } else { 30815 result = inspectTs_(bytes); 30816 } 30817 30818 if (!result || !result.audio && !result.video) { 30819 return null; 30820 } 30821 30822 adjustTimestamp_(result, baseTimestamp); 30823 30824 return result; 30825 }; 30826 30827 var tsInspector = { 30828 inspect: inspect 30829 }; 30830 30831 /* 30832 * pkcs7.pad 30833 * https://github.com/brightcove/pkcs7 30834 * 30835 * Copyright (c) 2014 Brightcove
vendor: 121,872 bytes, lines 30836-34502
30836 * Licensed under the apache2 license. 30837 */ 30838 30839 /** 30840 * Returns the subarray of a Uint8Array without PKCS#7 padding. 30841 * @param padded {Uint8Array} unencrypted bytes that have been padded 30842 * @return {Uint8Array} the unpadded bytes 30843 * @see http://tools.ietf.org/html/rfc5652 30844 */ 30845 function unpad(padded) { 30846 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 30847 } 30848 30849 var classCallCheck$2 = function classCallCheck$$1(instance, Constructor) { 30850 if (!(instance instanceof Constructor)) { 30851 throw new TypeError("Cannot call a class as a function"); 30852 } 30853 }; 30854 30855 var createClass$1 = function () { 30856 function defineProperties(target, props) { 30857 for (var i = 0; i < props.length; i++) { 30858 var descriptor = props[i]; 30859 descriptor.enumerable = descriptor.enumerable || false; 30860 descriptor.configurable = true; 30861 if ("value" in descriptor) descriptor.writable = true; 30862 Object.defineProperty(target, descriptor.key, descriptor); 30863 } 30864 } 30865 30866 return function (Constructor, protoProps, staticProps) { 30867 if (protoProps) defineProperties(Constructor.prototype, protoProps); 30868 if (staticProps) defineProperties(Constructor, staticProps); 30869 return Constructor; 30870 }; 30871 }(); 30872 30873 var inherits$2 = function inherits$$1(subClass, superClass) { 30874 if (typeof superClass !== "function" && superClass !== null) { 30875 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === "undefined" ? "undefined" : _typeof(superClass))); 30876 } 30877 30878 subClass.prototype = Object.create(superClass && superClass.prototype, { 30879 constructor: { 30880 value: subClass, 30881 enumerable: false, 30882 writable: true, 30883 configurable: true 30884 } 30885 }); 30886 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 30887 }; 30888 30889 var possibleConstructorReturn$2 = function possibleConstructorReturn$$1(self, call) { 30890 if (!self) { 30891 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 30892 } 30893 30894 return call && ((typeof call === "undefined" ? "undefined" : _typeof(call)) === "object" || typeof call === "function") ? call : self; 30895 }; 30896 30897 /** 30898 * @file aes.js 30899 * 30900 * This file contains an adaptation of the AES decryption algorithm 30901 * from the Standford Javascript Cryptography Library. That work is 30902 * covered by the following copyright and permissions notice: 30903 * 30904 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 30905 * All rights reserved. 30906 * 30907 * Redistribution and use in source and binary forms, with or without 30908 * modification, are permitted provided that the following conditions are 30909 * met: 30910 * 30911 * 1. Redistributions of source code must retain the above copyright 30912 * notice, this list of conditions and the following disclaimer. 30913 * 30914 * 2. Redistributions in binary form must reproduce the above 30915 * copyright notice, this list of conditions and the following 30916 * disclaimer in the documentation and/or other materials provided 30917 * with the distribution. 30918 * 30919 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 30920 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 30921 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30922 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 30923 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30924 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30925 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 30926 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30927 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 30928 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 30929 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30930 * 30931 * The views and conclusions contained in the software and documentation 30932 * are those of the authors and should not be interpreted as representing 30933 * official policies, either expressed or implied, of the authors. 30934 */ 30935 30936 /** 30937 * Expand the S-box tables. 30938 * 30939 * @private 30940 */ 30941 var precompute = function precompute() { 30942 var tables = [[[], [], [], [], []], [[], [], [], [], []]]; 30943 var encTable = tables[0]; 30944 var decTable = tables[1]; 30945 var sbox = encTable[4]; 30946 var sboxInv = decTable[4]; 30947 var i = void 0; 30948 var x = void 0; 30949 var xInv = void 0; 30950 var d = []; 30951 var th = []; 30952 var x2 = void 0; 30953 var x4 = void 0; 30954 var x8 = void 0; 30955 var s = void 0; 30956 var tEnc = void 0; 30957 var tDec = void 0; 30958 30959 // Compute double and third tables 30960 for (i = 0; i < 256; i++) { 30961 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 30962 } 30963 30964 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 30965 // Compute sbox 30966 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 30967 s = s >> 8 ^ s & 255 ^ 99; 30968 sbox[x] = s; 30969 sboxInv[s] = x; 30970 30971 // Compute MixColumns 30972 x8 = d[x4 = d[x2 = d[x]]]; 30973 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 30974 tEnc = d[s] * 0x101 ^ s * 0x1010100; 30975 30976 for (i = 0; i < 4; i++) { 30977 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 30978 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 30979 } 30980 } 30981 30982 // Compactify. Considerable speedup on Firefox. 30983 for (i = 0; i < 5; i++) { 30984 encTable[i] = encTable[i].slice(0); 30985 decTable[i] = decTable[i].slice(0); 30986 } 30987 return tables; 30988 }; 30989 var aesTables = null; 30990 30991 /** 30992 * Schedule out an AES key for both encryption and decryption. This 30993 * is a low-level class. Use a cipher mode to do bulk encryption. 30994 * 30995 * @class AES 30996 * @param key {Array} The key as an array of 4, 6 or 8 words. 30997 */ 30998 30999 var AES = function () { 31000 function AES(key) { 31001 classCallCheck$2(this, AES); 31002 31003 /** 31004 * The expanded S-box and inverse S-box tables. These will be computed 31005 * on the client so that we don't have to send them down the wire. 31006 * 31007 * There are two tables, _tables[0] is for encryption and 31008 * _tables[1] is for decryption. 31009 * 31010 * The first 4 sub-tables are the expanded S-box with MixColumns. The 31011 * last (_tables[01][4]) is the S-box itself. 31012 * 31013 * @private 31014 */ 31015 // if we have yet to precompute the S-box tables 31016 // do so now 31017 if (!aesTables) { 31018 aesTables = precompute(); 31019 } 31020 // then make a copy of that object for use 31021 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()]]; 31022 var i = void 0; 31023 var j = void 0; 31024 var tmp = void 0; 31025 var encKey = void 0; 31026 var decKey = void 0; 31027 var sbox = this._tables[0][4]; 31028 var decTable = this._tables[1]; 31029 var keyLen = key.length; 31030 var rcon = 1; 31031 31032 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 31033 throw new Error('Invalid aes key size'); 31034 } 31035 31036 encKey = key.slice(0); 31037 decKey = []; 31038 this._key = [encKey, decKey]; 31039 31040 // schedule encryption keys 31041 for (i = keyLen; i < 4 * keyLen + 28; i++) { 31042 tmp = encKey[i - 1]; 31043 31044 // apply sbox 31045 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 31046 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; 31047 31048 // shift rows and add rcon 31049 if (i % keyLen === 0) { 31050 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 31051 rcon = rcon << 1 ^ (rcon >> 7) * 283; 31052 } 31053 } 31054 31055 encKey[i] = encKey[i - keyLen] ^ tmp; 31056 } 31057 31058 // schedule decryption keys 31059 for (j = 0; i; j++, i--) { 31060 tmp = encKey[j & 3 ? i : i - 4]; 31061 if (i <= 4 || j < 4) { 31062 decKey[j] = tmp; 31063 } else { 31064 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 31065 } 31066 } 31067 } 31068 31069 /** 31070 * Decrypt 16 bytes, specified as four 32-bit words. 31071 * 31072 * @param {Number} encrypted0 the first word to decrypt 31073 * @param {Number} encrypted1 the second word to decrypt 31074 * @param {Number} encrypted2 the third word to decrypt 31075 * @param {Number} encrypted3 the fourth word to decrypt 31076 * @param {Int32Array} out the array to write the decrypted words 31077 * into 31078 * @param {Number} offset the offset into the output array to start 31079 * writing results 31080 * @return {Array} The plaintext. 31081 */ 31082 31083 AES.prototype.decrypt = function decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 31084 var key = this._key[1]; 31085 // state variables a,b,c,d are loaded with pre-whitened data 31086 var a = encrypted0 ^ key[0]; 31087 var b = encrypted3 ^ key[1]; 31088 var c = encrypted2 ^ key[2]; 31089 var d = encrypted1 ^ key[3]; 31090 var a2 = void 0; 31091 var b2 = void 0; 31092 var c2 = void 0; 31093 31094 // key.length === 2 ? 31095 var nInnerRounds = key.length / 4 - 2; 31096 var i = void 0; 31097 var kIndex = 4; 31098 var table = this._tables[1]; 31099 31100 // load up the tables 31101 var table0 = table[0]; 31102 var table1 = table[1]; 31103 var table2 = table[2]; 31104 var table3 = table[3]; 31105 var sbox = table[4]; 31106 31107 // Inner rounds. Cribbed from OpenSSL. 31108 for (i = 0; i < nInnerRounds; i++) { 31109 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 31110 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 31111 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 31112 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 31113 kIndex += 4; 31114 a = a2;b = b2;c = c2; 31115 } 31116 31117 // Last round. 31118 for (i = 0; i < 4; i++) { 31119 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 31120 a2 = a;a = b;b = c;c = d;d = a2; 31121 } 31122 }; 31123 31124 return AES; 31125 }(); 31126 31127 /** 31128 * @file stream.js 31129 */ 31130 /** 31131 * A lightweight readable stream implemention that handles event dispatching. 31132 * 31133 * @class Stream 31134 */ 31135 var Stream$3 = function () { 31136 function Stream() { 31137 classCallCheck$2(this, Stream); 31138 31139 this.listeners = {}; 31140 } 31141 31142 /** 31143 * Add a listener for a specified event type. 31144 * 31145 * @param {String} type the event name 31146 * @param {Function} listener the callback to be invoked when an event of 31147 * the specified type occurs 31148 */ 31149 31150 Stream.prototype.on = function on(type, listener) { 31151 if (!this.listeners[type]) { 31152 this.listeners[type] = []; 31153 } 31154 this.listeners[type].push(listener); 31155 }; 31156 31157 /** 31158 * Remove a listener for a specified event type. 31159 * 31160 * @param {String} type the event name 31161 * @param {Function} listener a function previously registered for this 31162 * type of event through `on` 31163 * @return {Boolean} if we could turn it off or not 31164 */ 31165 31166 Stream.prototype.off = function off(type, listener) { 31167 if (!this.listeners[type]) { 31168 return false; 31169 } 31170 31171 var index = this.listeners[type].indexOf(listener); 31172 31173 this.listeners[type].splice(index, 1); 31174 return index > -1; 31175 }; 31176 31177 /** 31178 * Trigger an event of the specified type on this stream. Any additional 31179 * arguments to this function are passed as parameters to event listeners. 31180 * 31181 * @param {String} type the event name 31182 */ 31183 31184 Stream.prototype.trigger = function trigger(type) { 31185 var callbacks = this.listeners[type]; 31186 31187 if (!callbacks) { 31188 return; 31189 } 31190 31191 // Slicing the arguments on every invocation of this method 31192 // can add a significant amount of overhead. Avoid the 31193 // intermediate object creation for the common case of a 31194 // single callback argument 31195 if (arguments.length === 2) { 31196 var length = callbacks.length; 31197 31198 for (var i = 0; i < length; ++i) { 31199 callbacks[i].call(this, arguments[1]); 31200 } 31201 } else { 31202 var args = Array.prototype.slice.call(arguments, 1); 31203 var _length = callbacks.length; 31204 31205 for (var _i = 0; _i < _length; ++_i) { 31206 callbacks[_i].apply(this, args); 31207 } 31208 } 31209 }; 31210 31211 /** 31212 * Destroys the stream and cleans up. 31213 */ 31214 31215 Stream.prototype.dispose = function dispose() { 31216 this.listeners = {}; 31217 }; 31218 /** 31219 * Forwards all `data` events on this stream to the destination stream. The 31220 * destination stream should provide a method `push` to receive the data 31221 * events as they arrive. 31222 * 31223 * @param {Stream} destination the stream that will receive all `data` events 31224 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 31225 */ 31226 31227 Stream.prototype.pipe = function pipe(destination) { 31228 this.on('data', function (data) { 31229 destination.push(data); 31230 }); 31231 }; 31232 31233 return Stream; 31234 }(); 31235 31236 /** 31237 * @file async-stream.js 31238 */ 31239 /** 31240 * A wrapper around the Stream class to use setTiemout 31241 * and run stream "jobs" Asynchronously 31242 * 31243 * @class AsyncStream 31244 * @extends Stream 31245 */ 31246 31247 var AsyncStream = function (_Stream) { 31248 inherits$2(AsyncStream, _Stream); 31249 31250 function AsyncStream() { 31251 classCallCheck$2(this, AsyncStream); 31252 31253 var _this = possibleConstructorReturn$2(this, _Stream.call(this, Stream$3)); 31254 31255 _this.jobs = []; 31256 _this.delay = 1; 31257 _this.timeout_ = null; 31258 return _this; 31259 } 31260 31261 /** 31262 * process an async job 31263 * 31264 * @private 31265 */ 31266 31267 AsyncStream.prototype.processJob_ = function processJob_() { 31268 this.jobs.shift()(); 31269 if (this.jobs.length) { 31270 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 31271 } else { 31272 this.timeout_ = null; 31273 } 31274 }; 31275 31276 /** 31277 * push a job into the stream 31278 * 31279 * @param {Function} job the job to push into the stream 31280 */ 31281 31282 AsyncStream.prototype.push = function push(job) { 31283 this.jobs.push(job); 31284 if (!this.timeout_) { 31285 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 31286 } 31287 }; 31288 31289 return AsyncStream; 31290 }(Stream$3); 31291 31292 /** 31293 * @file decrypter.js 31294 * 31295 * An asynchronous implementation of AES-128 CBC decryption with 31296 * PKCS#7 padding. 31297 */ 31298 31299 /** 31300 * Convert network-order (big-endian) bytes into their little-endian 31301 * representation. 31302 */ 31303 var ntoh = function ntoh(word) { 31304 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 31305 }; 31306 31307 /** 31308 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 31309 * 31310 * @param {Uint8Array} encrypted the encrypted bytes 31311 * @param {Uint32Array} key the bytes of the decryption key 31312 * @param {Uint32Array} initVector the initialization vector (IV) to 31313 * use for the first round of CBC. 31314 * @return {Uint8Array} the decrypted bytes 31315 * 31316 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 31317 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 31318 * @see https://tools.ietf.org/html/rfc2315 31319 */ 31320 var decrypt = function decrypt(encrypted, key, initVector) { 31321 // word-level access to the encrypted bytes 31322 var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 31323 31324 var decipher = new AES(Array.prototype.slice.call(key)); 31325 31326 // byte and word-level access for the decrypted output 31327 var decrypted = new Uint8Array(encrypted.byteLength); 31328 var decrypted32 = new Int32Array(decrypted.buffer); 31329 31330 // temporary variables for working with the IV, encrypted, and 31331 // decrypted data 31332 var init0 = void 0; 31333 var init1 = void 0; 31334 var init2 = void 0; 31335 var init3 = void 0; 31336 var encrypted0 = void 0; 31337 var encrypted1 = void 0; 31338 var encrypted2 = void 0; 31339 var encrypted3 = void 0; 31340 31341 // iteration variable 31342 var wordIx = void 0; 31343 31344 // pull out the words of the IV to ensure we don't modify the 31345 // passed-in reference and easier access 31346 init0 = initVector[0]; 31347 init1 = initVector[1]; 31348 init2 = initVector[2]; 31349 init3 = initVector[3]; 31350 31351 // decrypt four word sequences, applying cipher-block chaining (CBC) 31352 // to each decrypted block 31353 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 31354 // convert big-endian (network order) words into little-endian 31355 // (javascript order) 31356 encrypted0 = ntoh(encrypted32[wordIx]); 31357 encrypted1 = ntoh(encrypted32[wordIx + 1]); 31358 encrypted2 = ntoh(encrypted32[wordIx + 2]); 31359 encrypted3 = ntoh(encrypted32[wordIx + 3]); 31360 31361 // decrypt the block 31362 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); 31363 31364 // XOR with the IV, and restore network byte-order to obtain the 31365 // plaintext 31366 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 31367 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 31368 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 31369 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); 31370 31371 // setup the IV for the next round 31372 init0 = encrypted0; 31373 init1 = encrypted1; 31374 init2 = encrypted2; 31375 init3 = encrypted3; 31376 } 31377 31378 return decrypted; 31379 }; 31380 31381 /** 31382 * The `Decrypter` class that manages decryption of AES 31383 * data through `AsyncStream` objects and the `decrypt` 31384 * function 31385 * 31386 * @param {Uint8Array} encrypted the encrypted bytes 31387 * @param {Uint32Array} key the bytes of the decryption key 31388 * @param {Uint32Array} initVector the initialization vector (IV) to 31389 * @param {Function} done the function to run when done 31390 * @class Decrypter 31391 */ 31392 31393 var Decrypter = function () { 31394 function Decrypter(encrypted, key, initVector, done) { 31395 classCallCheck$2(this, Decrypter); 31396 31397 var step = Decrypter.STEP; 31398 var encrypted32 = new Int32Array(encrypted.buffer); 31399 var decrypted = new Uint8Array(encrypted.byteLength); 31400 var i = 0; 31401 31402 this.asyncStream_ = new AsyncStream(); 31403 31404 // split up the encryption job and do the individual chunks asynchronously 31405 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 31406 for (i = step; i < encrypted32.length; i += step) { 31407 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 31408 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 31409 } 31410 // invoke the done() callback when everything is finished 31411 this.asyncStream_.push(function () { 31412 // remove pkcs#7 padding from the decrypted bytes 31413 done(null, unpad(decrypted)); 31414 }); 31415 } 31416 31417 /** 31418 * a getter for step the maximum number of bytes to process at one time 31419 * 31420 * @return {Number} the value of step 32000 31421 */ 31422 31423 /** 31424 * @private 31425 */ 31426 Decrypter.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { 31427 return function () { 31428 var bytes = decrypt(encrypted, key, initVector); 31429 31430 decrypted.set(bytes, encrypted.byteOffset); 31431 }; 31432 }; 31433 31434 createClass$1(Decrypter, null, [{ 31435 key: 'STEP', 31436 get: function get$$1() { 31437 // 4 * 8000; 31438 return 32000; 31439 } 31440 }]); 31441 return Decrypter; 31442 }(); 31443 31444 /** 31445 * @videojs/http-streaming 31446 * @version 1.0.2 31447 * @copyright 2018 Brightcove, Inc 31448 * @license Apache-2.0 31449 */ 31450 31451 /** 31452 * @file resolve-url.js 31453 */ 31454 31455 var resolveUrl$1 = function resolveUrl(baseURL, relativeURL) { 31456 // return early if we don't need to resolve 31457 if (/^[a-z]+:/i.test(relativeURL)) { 31458 return relativeURL; 31459 } 31460 31461 // if the base URL is relative then combine with the current location 31462 if (!/\/\//i.test(baseURL)) { 31463 baseURL = urlToolkit.buildAbsoluteURL(window_1.location.href, baseURL); 31464 } 31465 31466 return urlToolkit.buildAbsoluteURL(baseURL, relativeURL); 31467 }; 31468 31469 var classCallCheck$3 = function classCallCheck$$1(instance, Constructor) { 31470 if (!(instance instanceof Constructor)) { 31471 throw new TypeError("Cannot call a class as a function"); 31472 } 31473 }; 31474 31475 var createClass$2 = function () { 31476 function defineProperties(target, props) { 31477 for (var i = 0; i < props.length; i++) { 31478 var descriptor = props[i]; 31479 descriptor.enumerable = descriptor.enumerable || false; 31480 descriptor.configurable = true; 31481 if ("value" in descriptor) descriptor.writable = true; 31482 Object.defineProperty(target, descriptor.key, descriptor); 31483 } 31484 } 31485 31486 return function (Constructor, protoProps, staticProps) { 31487 if (protoProps) defineProperties(Constructor.prototype, protoProps); 31488 if (staticProps) defineProperties(Constructor, staticProps); 31489 return Constructor; 31490 }; 31491 }(); 31492 31493 var get$2 = function get$$1(object, property, receiver) { 31494 if (object === null) object = Function.prototype; 31495 var desc = Object.getOwnPropertyDescriptor(object, property); 31496 31497 if (desc === undefined) { 31498 var parent = Object.getPrototypeOf(object); 31499 31500 if (parent === null) { 31501 return undefined; 31502 } else { 31503 return get$$1(parent, property, receiver); 31504 } 31505 } else if ("value" in desc) { 31506 return desc.value; 31507 } else { 31508 var getter = desc.get; 31509 31510 if (getter === undefined) { 31511 return undefined; 31512 } 31513 31514 return getter.call(receiver); 31515 } 31516 }; 31517 31518 var inherits$3 = function inherits$$1(subClass, superClass) { 31519 if (typeof superClass !== "function" && superClass !== null) { 31520 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === 'undefined' ? 'undefined' : _typeof(superClass))); 31521 } 31522 31523 subClass.prototype = Object.create(superClass && superClass.prototype, { 31524 constructor: { 31525 value: subClass, 31526 enumerable: false, 31527 writable: true, 31528 configurable: true 31529 } 31530 }); 31531 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 31532 }; 31533 31534 var possibleConstructorReturn$3 = function possibleConstructorReturn$$1(self, call) { 31535 if (!self) { 31536 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 31537 } 31538 31539 return call && ((typeof call === 'undefined' ? 'undefined' : _typeof(call)) === "object" || typeof call === "function") ? call : self; 31540 }; 31541 31542 var slicedToArray$1 = function () { 31543 function sliceIterator(arr, i) { 31544 var _arr = []; 31545 var _n = true; 31546 var _d = false; 31547 var _e = undefined; 31548 31549 try { 31550 for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 31551 _arr.push(_s.value); 31552 31553 if (i && _arr.length === i) break; 31554 } 31555 } catch (err) { 31556 _d = true; 31557 _e = err; 31558 } finally { 31559 try { 31560 if (!_n && _i["return"]) _i["return"](); 31561 } finally { 31562 if (_d) throw _e; 31563 } 31564 } 31565 31566 return _arr; 31567 } 31568 31569 return function (arr, i) { 31570 if (Array.isArray(arr)) { 31571 return arr; 31572 } else if (Symbol.iterator in Object(arr)) { 31573 return sliceIterator(arr, i); 31574 } else { 31575 throw new TypeError("Invalid attempt to destructure non-iterable instance"); 31576 } 31577 }; 31578 }(); 31579 31580 /** 31581 * @file playlist-loader.js 31582 * 31583 * A state machine that manages the loading, caching, and updating of 31584 * M3U8 playlists. 31585 * 31586 */ 31587 31588 var mergeOptions$1 = videojs$1.mergeOptions, 31589 EventTarget$1 = videojs$1.EventTarget, 31590 log$2 = videojs$1.log; 31591 31592 /** 31593 * Loops through all supported media groups in master and calls the provided 31594 * callback for each group 31595 * 31596 * @param {Object} master 31597 * The parsed master manifest object 31598 * @param {Function} callback 31599 * Callback to call for each media group 31600 */ 31601 31602 var forEachMediaGroup = function forEachMediaGroup(master, callback) { 31603 ['AUDIO', 'SUBTITLES'].forEach(function (mediaType) { 31604 for (var groupKey in master.mediaGroups[mediaType]) { 31605 for (var labelKey in master.mediaGroups[mediaType][groupKey]) { 31606 var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey]; 31607 31608 callback(mediaProperties, mediaType, groupKey, labelKey); 31609 } 31610 } 31611 }); 31612 }; 31613 31614 /** 31615 * Returns a new array of segments that is the result of merging 31616 * properties from an older list of segments onto an updated 31617 * list. No properties on the updated playlist will be overridden. 31618 * 31619 * @param {Array} original the outdated list of segments 31620 * @param {Array} update the updated list of segments 31621 * @param {Number=} offset the index of the first update 31622 * segment in the original segment list. For non-live playlists, 31623 * this should always be zero and does not need to be 31624 * specified. For live playlists, it should be the difference 31625 * between the media sequence numbers in the original and updated 31626 * playlists. 31627 * @return a list of merged segment objects 31628 */ 31629 var updateSegments = function updateSegments(original, update, offset) { 31630 var result = update.slice(); 31631 31632 offset = offset || 0; 31633 var length = Math.min(original.length, update.length + offset); 31634 31635 for (var i = offset; i < length; i++) { 31636 result[i - offset] = mergeOptions$1(original[i], result[i - offset]); 31637 } 31638 return result; 31639 }; 31640 31641 var resolveSegmentUris = function resolveSegmentUris(segment, baseUri) { 31642 if (!segment.resolvedUri) { 31643 segment.resolvedUri = resolveUrl$1(baseUri, segment.uri); 31644 } 31645 if (segment.key && !segment.key.resolvedUri) { 31646 segment.key.resolvedUri = resolveUrl$1(baseUri, segment.key.uri); 31647 } 31648 if (segment.map && !segment.map.resolvedUri) { 31649 segment.map.resolvedUri = resolveUrl$1(baseUri, segment.map.uri); 31650 } 31651 }; 31652 31653 /** 31654 * Returns a new master playlist that is the result of merging an 31655 * updated media playlist into the original version. If the 31656 * updated media playlist does not match any of the playlist 31657 * entries in the original master playlist, null is returned. 31658 * 31659 * @param {Object} master a parsed master M3U8 object 31660 * @param {Object} media a parsed media M3U8 object 31661 * @return {Object} a new object that represents the original 31662 * master playlist with the updated media playlist merged in, or 31663 * null if the merge produced no change. 31664 */ 31665 var updateMaster = function updateMaster(master, media) { 31666 var result = mergeOptions$1(master, {}); 31667 var playlist = result.playlists[media.uri]; 31668 31669 if (!playlist) { 31670 return null; 31671 } 31672 31673 // consider the playlist unchanged if the number of segments is equal and the media 31674 // sequence number is unchanged 31675 if (playlist.segments && media.segments && playlist.segments.length === media.segments.length && playlist.mediaSequence === media.mediaSequence) { 31676 return null; 31677 } 31678 31679 var mergedPlaylist = mergeOptions$1(playlist, media); 31680 31681 // if the update could overlap existing segment information, merge the two segment lists 31682 if (playlist.segments) { 31683 mergedPlaylist.segments = updateSegments(playlist.segments, media.segments, media.mediaSequence - playlist.mediaSequence); 31684 } 31685 31686 // resolve any segment URIs to prevent us from having to do it later 31687 mergedPlaylist.segments.forEach(function (segment) { 31688 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 31689 }); 31690 31691 // TODO Right now in the playlists array there are two references to each playlist, one 31692 // that is referenced by index, and one by URI. The index reference may no longer be 31693 // necessary. 31694 for (var i = 0; i < result.playlists.length; i++) { 31695 if (result.playlists[i].uri === media.uri) { 31696 result.playlists[i] = mergedPlaylist; 31697 } 31698 } 31699 result.playlists[media.uri] = mergedPlaylist; 31700 31701 return result; 31702 }; 31703 31704 var setupMediaPlaylists = function setupMediaPlaylists(master) { 31705 // setup by-URI lookups and resolve media playlist URIs 31706 var i = master.playlists.length; 31707 31708 while (i--) { 31709 var playlist = master.playlists[i]; 31710 31711 master.playlists[playlist.uri] = playlist; 31712 playlist.resolvedUri = resolveUrl$1(master.uri, playlist.uri); 31713 playlist.id = i; 31714 31715 if (!playlist.attributes) { 31716 // Although the spec states an #EXT-X-STREAM-INF tag MUST have a 31717 // BANDWIDTH attribute, we can play the stream without it. This means a poorly 31718 // formatted master playlist may not have an attribute list. An attributes 31719 // property is added here to prevent undefined references when we encounter 31720 // this scenario. 31721 playlist.attributes = {}; 31722 31723 log$2.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 31724 } 31725 } 31726 }; 31727 31728 var resolveMediaGroupUris = function resolveMediaGroupUris(master) { 31729 forEachMediaGroup(master, function (properties) { 31730 if (properties.uri) { 31731 properties.resolvedUri = resolveUrl$1(master.uri, properties.uri); 31732 } 31733 }); 31734 }; 31735 31736 /** 31737 * Calculates the time to wait before refreshing a live playlist 31738 * 31739 * @param {Object} media 31740 * The current media 31741 * @param {Boolean} update 31742 * True if there were any updates from the last refresh, false otherwise 31743 * @return {Number} 31744 * The time in ms to wait before refreshing the live playlist 31745 */ 31746 var refreshDelay = function refreshDelay(media, update) { 31747 var lastSegment = media.segments[media.segments.length - 1]; 31748 var delay = void 0; 31749 31750 if (update && lastSegment && lastSegment.duration) { 31751 delay = lastSegment.duration * 1000; 31752 } else { 31753 // if the playlist is unchanged since the last reload or last segment duration 31754 // cannot be determined, try again after half the target duration 31755 delay = (media.targetDuration || 10) * 500; 31756 } 31757 return delay; 31758 }; 31759 31760 /** 31761 * Load a playlist from a remote location 31762 * 31763 * @class PlaylistLoader 31764 * @extends Stream 31765 * @param {String} srcUrl the url to start with 31766 * @param {Boolean} withCredentials the withCredentials xhr option 31767 * @constructor 31768 */ 31769 31770 var PlaylistLoader = function (_EventTarget) { 31771 inherits$3(PlaylistLoader, _EventTarget); 31772 31773 function PlaylistLoader(srcUrl, hls, withCredentials) { 31774 classCallCheck$3(this, PlaylistLoader); 31775 31776 var _this = possibleConstructorReturn$3(this, (PlaylistLoader.__proto__ || Object.getPrototypeOf(PlaylistLoader)).call(this)); 31777 31778 _this.srcUrl = srcUrl; 31779 _this.hls_ = hls; 31780 _this.withCredentials = withCredentials; 31781 31782 if (!_this.srcUrl) { 31783 throw new Error('A non-empty playlist URL is required'); 31784 } 31785 31786 // initialize the loader state 31787 _this.state = 'HAVE_NOTHING'; 31788 31789 // live playlist staleness timeout 31790 _this.on('mediaupdatetimeout', function () { 31791 if (_this.state !== 'HAVE_METADATA') { 31792 // only refresh the media playlist if no other activity is going on 31793 return; 31794 } 31795 31796 _this.state = 'HAVE_CURRENT_METADATA'; 31797 31798 _this.request = _this.hls_.xhr({ 31799 uri: resolveUrl$1(_this.master.uri, _this.media().uri), 31800 withCredentials: _this.withCredentials 31801 }, function (error, req) { 31802 // disposed 31803 if (!_this.request) { 31804 return; 31805 } 31806 31807 if (error) { 31808 return _this.playlistRequestError(_this.request, _this.media().uri, 'HAVE_METADATA'); 31809 } 31810 31811 _this.haveMetadata(_this.request, _this.media().uri); 31812 }); 31813 }); 31814 return _this; 31815 } 31816 31817 createClass$2(PlaylistLoader, [{ 31818 key: 'playlistRequestError', 31819 value: function playlistRequestError(xhr, url, startingState) { 31820 // any in-flight request is now finished 31821 this.request = null; 31822 31823 if (startingState) { 31824 this.state = startingState; 31825 } 31826 31827 this.error = { 31828 playlist: this.master.playlists[url], 31829 status: xhr.status, 31830 message: 'HLS playlist request error at URL: ' + url, 31831 responseText: xhr.responseText, 31832 code: xhr.status >= 500 ? 4 : 2 31833 }; 31834 31835 this.trigger('error'); 31836 } 31837 31838 // update the playlist loader's state in response to a new or 31839 // updated playlist. 31840 31841 }, { 31842 key: 'haveMetadata', 31843 value: function haveMetadata(xhr, url) { 31844 var _this2 = this; 31845 31846 // any in-flight request is now finished 31847 this.request = null; 31848 this.state = 'HAVE_METADATA'; 31849 31850 var parser = new Parser(); 31851 31852 parser.push(xhr.responseText); 31853 parser.end(); 31854 parser.manifest.uri = url; 31855 // m3u8-parser does not attach an attributes property to media playlists so make 31856 // sure that the property is attached to avoid undefined reference errors 31857 parser.manifest.attributes = parser.manifest.attributes || {}; 31858 31859 // merge this playlist into the master 31860 var update = updateMaster(this.master, parser.manifest); 31861 31862 this.targetDuration = parser.manifest.targetDuration; 31863 31864 if (update) { 31865 this.master = update; 31866 this.media_ = this.master.playlists[parser.manifest.uri]; 31867 } else { 31868 this.trigger('playlistunchanged'); 31869 } 31870 31871 // refresh live playlists after a target duration passes 31872 if (!this.media().endList) { 31873 window_1.clearTimeout(this.mediaUpdateTimeout); 31874 this.mediaUpdateTimeout = window_1.setTimeout(function () { 31875 _this2.trigger('mediaupdatetimeout'); 31876 }, refreshDelay(this.media(), !!update)); 31877 } 31878 31879 this.trigger('loadedplaylist'); 31880 } 31881 31882 /** 31883 * Abort any outstanding work and clean up. 31884 */ 31885 31886 }, { 31887 key: 'dispose', 31888 value: function dispose() { 31889 this.stopRequest(); 31890 window_1.clearTimeout(this.mediaUpdateTimeout); 31891 } 31892 }, { 31893 key: 'stopRequest', 31894 value: function stopRequest() { 31895 if (this.request) { 31896 var oldRequest = this.request; 31897 31898 this.request = null; 31899 oldRequest.onreadystatechange = null; 31900 oldRequest.abort(); 31901 } 31902 } 31903 31904 /** 31905 * When called without any arguments, returns the currently 31906 * active media playlist. When called with a single argument, 31907 * triggers the playlist loader to asynchronously switch to the 31908 * specified media playlist. Calling this method while the 31909 * loader is in the HAVE_NOTHING causes an error to be emitted 31910 * but otherwise has no effect. 31911 * 31912 * @param {Object=} playlist the parsed media playlist 31913 * object to switch to 31914 * @return {Playlist} the current loaded media 31915 */ 31916 31917 }, { 31918 key: 'media', 31919 value: function media(playlist) { 31920 var _this3 = this; 31921 31922 // getter 31923 if (!playlist) { 31924 return this.media_; 31925 } 31926 31927 // setter 31928 if (this.state === 'HAVE_NOTHING') { 31929 throw new Error('Cannot switch media playlist from ' + this.state); 31930 } 31931 31932 var startingState = this.state; 31933 31934 // find the playlist object if the target playlist has been 31935 // specified by URI 31936 if (typeof playlist === 'string') { 31937 if (!this.master.playlists[playlist]) { 31938 throw new Error('Unknown playlist URI: ' + playlist); 31939 } 31940 playlist = this.master.playlists[playlist]; 31941 } 31942 31943 var mediaChange = !this.media_ || playlist.uri !== this.media_.uri; 31944 31945 // switch to fully loaded playlists immediately 31946 if (this.master.playlists[playlist.uri].endList) { 31947 // abort outstanding playlist requests 31948 if (this.request) { 31949 this.request.onreadystatechange = null; 31950 this.request.abort(); 31951 this.request = null; 31952 } 31953 this.state = 'HAVE_METADATA'; 31954 this.media_ = playlist; 31955 31956 // trigger media change if the active media has been updated 31957 if (mediaChange) { 31958 this.trigger('mediachanging'); 31959 this.trigger('mediachange'); 31960 } 31961 return; 31962 } 31963 31964 // switching to the active playlist is a no-op 31965 if (!mediaChange) { 31966 return; 31967 } 31968 31969 this.state = 'SWITCHING_MEDIA'; 31970 31971 // there is already an outstanding playlist request 31972 if (this.request) { 31973 if (resolveUrl$1(this.master.uri, playlist.uri) === this.request.url) { 31974 // requesting to switch to the same playlist multiple times 31975 // has no effect after the first 31976 return; 31977 } 31978 this.request.onreadystatechange = null; 31979 this.request.abort(); 31980 this.request = null; 31981 } 31982 31983 // request the new playlist 31984 if (this.media_) { 31985 this.trigger('mediachanging'); 31986 } 31987 31988 this.request = this.hls_.xhr({ 31989 uri: resolveUrl$1(this.master.uri, playlist.uri), 31990 withCredentials: this.withCredentials 31991 }, function (error, req) { 31992 // disposed 31993 if (!_this3.request) { 31994 return; 31995 } 31996 31997 if (error) { 31998 return _this3.playlistRequestError(_this3.request, playlist.uri, startingState); 31999 } 32000 32001 _this3.haveMetadata(req, playlist.uri); 32002 32003 // fire loadedmetadata the first time a media playlist is loaded 32004 if (startingState === 'HAVE_MASTER') { 32005 _this3.trigger('loadedmetadata'); 32006 } else { 32007 _this3.trigger('mediachange'); 32008 } 32009 }); 32010 } 32011 32012 /** 32013 * pause loading of the playlist 32014 */ 32015 32016 }, { 32017 key: 'pause', 32018 value: function pause() { 32019 this.stopRequest(); 32020 window_1.clearTimeout(this.mediaUpdateTimeout); 32021 if (this.state === 'HAVE_NOTHING') { 32022 // If we pause the loader before any data has been retrieved, its as if we never 32023 // started, so reset to an unstarted state. 32024 this.started = false; 32025 } 32026 // Need to restore state now that no activity is happening 32027 if (this.state === 'SWITCHING_MEDIA') { 32028 // if the loader was in the process of switching media, it should either return to 32029 // HAVE_MASTER or HAVE_METADATA depending on if the loader has loaded a media 32030 // playlist yet. This is determined by the existence of loader.media_ 32031 if (this.media_) { 32032 this.state = 'HAVE_METADATA'; 32033 } else { 32034 this.state = 'HAVE_MASTER'; 32035 } 32036 } else if (this.state === 'HAVE_CURRENT_METADATA') { 32037 this.state = 'HAVE_METADATA'; 32038 } 32039 } 32040 32041 /** 32042 * start loading of the playlist 32043 */ 32044 32045 }, { 32046 key: 'load', 32047 value: function load(isFinalRendition) { 32048 var _this4 = this; 32049 32050 window_1.clearTimeout(this.mediaUpdateTimeout); 32051 32052 var media = this.media(); 32053 32054 if (isFinalRendition) { 32055 var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 32056 32057 this.mediaUpdateTimeout = window_1.setTimeout(function () { 32058 return _this4.load(); 32059 }, delay); 32060 return; 32061 } 32062 32063 if (!this.started) { 32064 this.start(); 32065 return; 32066 } 32067 32068 if (media && !media.endList) { 32069 this.trigger('mediaupdatetimeout'); 32070 } else { 32071 this.trigger('loadedplaylist'); 32072 } 32073 } 32074 32075 /** 32076 * start loading of the playlist 32077 */ 32078 32079 }, { 32080 key: 'start', 32081 value: function start() { 32082 var _this5 = this; 32083 32084 this.started = true; 32085 32086 // request the specified URL 32087 this.request = this.hls_.xhr({ 32088 uri: this.srcUrl, 32089 withCredentials: this.withCredentials 32090 }, function (error, req) { 32091 // disposed 32092 if (!_this5.request) { 32093 return; 32094 } 32095 32096 // clear the loader's request reference 32097 _this5.request = null; 32098 32099 if (error) { 32100 _this5.error = { 32101 status: req.status, 32102 message: 'HLS playlist request error at URL: ' + _this5.srcUrl, 32103 responseText: req.responseText, 32104 // MEDIA_ERR_NETWORK 32105 code: 2 32106 }; 32107 if (_this5.state === 'HAVE_NOTHING') { 32108 _this5.started = false; 32109 } 32110 return _this5.trigger('error'); 32111 } 32112 32113 var parser = new Parser(); 32114 32115 parser.push(req.responseText); 32116 parser.end(); 32117 32118 _this5.state = 'HAVE_MASTER'; 32119 32120 parser.manifest.uri = _this5.srcUrl; 32121 32122 // loaded a master playlist 32123 if (parser.manifest.playlists) { 32124 _this5.master = parser.manifest; 32125 32126 setupMediaPlaylists(_this5.master); 32127 resolveMediaGroupUris(_this5.master); 32128 32129 _this5.trigger('loadedplaylist'); 32130 if (!_this5.request) { 32131 // no media playlist was specifically selected so start 32132 // from the first listed one 32133 _this5.media(parser.manifest.playlists[0]); 32134 } 32135 return; 32136 } 32137 32138 // loaded a media playlist 32139 // infer a master playlist if none was previously requested 32140 _this5.master = { 32141 mediaGroups: { 32142 'AUDIO': {}, 32143 'VIDEO': {}, 32144 'CLOSED-CAPTIONS': {}, 32145 'SUBTITLES': {} 32146 }, 32147 uri: window_1.location.href, 32148 playlists: [{ 32149 uri: _this5.srcUrl, 32150 id: 0 32151 }] 32152 }; 32153 _this5.master.playlists[_this5.srcUrl] = _this5.master.playlists[0]; 32154 _this5.master.playlists[0].resolvedUri = _this5.srcUrl; 32155 // m3u8-parser does not attach an attributes property to media playlists so make 32156 // sure that the property is attached to avoid undefined reference errors 32157 _this5.master.playlists[0].attributes = _this5.master.playlists[0].attributes || {}; 32158 _this5.haveMetadata(req, _this5.srcUrl); 32159 return _this5.trigger('loadedmetadata'); 32160 }); 32161 } 32162 }]); 32163 return PlaylistLoader; 32164 }(EventTarget$1); 32165 32166 /** 32167 * @file playlist.js 32168 * 32169 * Playlist related utilities. 32170 */ 32171 32172 var createTimeRange = videojs$1.createTimeRange; 32173 32174 /** 32175 * walk backward until we find a duration we can use 32176 * or return a failure 32177 * 32178 * @param {Playlist} playlist the playlist to walk through 32179 * @param {Number} endSequence the mediaSequence to stop walking on 32180 */ 32181 32182 var backwardDuration = function backwardDuration(playlist, endSequence) { 32183 var result = 0; 32184 var i = endSequence - playlist.mediaSequence; 32185 // if a start time is available for segment immediately following 32186 // the interval, use it 32187 var segment = playlist.segments[i]; 32188 32189 // Walk backward until we find the latest segment with timeline 32190 // information that is earlier than endSequence 32191 if (segment) { 32192 if (typeof segment.start !== 'undefined') { 32193 return { result: segment.start, precise: true }; 32194 } 32195 if (typeof segment.end !== 'undefined') { 32196 return { 32197 result: segment.end - segment.duration, 32198 precise: true 32199 }; 32200 } 32201 } 32202 while (i--) { 32203 segment = playlist.segments[i]; 32204 if (typeof segment.end !== 'undefined') { 32205 return { result: result + segment.end, precise: true }; 32206 } 32207 32208 result += segment.duration; 32209 32210 if (typeof segment.start !== 'undefined') { 32211 return { result: result + segment.start, precise: true }; 32212 } 32213 } 32214 return { result: result, precise: false }; 32215 }; 32216 32217 /** 32218 * walk forward until we find a duration we can use 32219 * or return a failure 32220 * 32221 * @param {Playlist} playlist the playlist to walk through 32222 * @param {Number} endSequence the mediaSequence to stop walking on 32223 */ 32224 var forwardDuration = function forwardDuration(playlist, endSequence) { 32225 var result = 0; 32226 var segment = void 0; 32227 var i = endSequence - playlist.mediaSequence; 32228 // Walk forward until we find the earliest segment with timeline 32229 // information 32230 32231 for (; i < playlist.segments.length; i++) { 32232 segment = playlist.segments[i]; 32233 if (typeof segment.start !== 'undefined') { 32234 return { 32235 result: segment.start - result, 32236 precise: true 32237 }; 32238 } 32239 32240 result += segment.duration; 32241 32242 if (typeof segment.end !== 'undefined') { 32243 return { 32244 result: segment.end - result, 32245 precise: true 32246 }; 32247 } 32248 } 32249 // indicate we didn't find a useful duration estimate 32250 return { result: -1, precise: false }; 32251 }; 32252 32253 /** 32254 * Calculate the media duration from the segments associated with a 32255 * playlist. The duration of a subinterval of the available segments 32256 * may be calculated by specifying an end index. 32257 * 32258 * @param {Object} playlist a media playlist object 32259 * @param {Number=} endSequence an exclusive upper boundary 32260 * for the playlist. Defaults to playlist length. 32261 * @param {Number} expired the amount of time that has dropped 32262 * off the front of the playlist in a live scenario 32263 * @return {Number} the duration between the first available segment 32264 * and end index. 32265 */ 32266 var intervalDuration = function intervalDuration(playlist, endSequence, expired) { 32267 var backward = void 0; 32268 var forward = void 0; 32269 32270 if (typeof endSequence === 'undefined') { 32271 endSequence = playlist.mediaSequence + playlist.segments.length; 32272 } 32273 32274 if (endSequence < playlist.mediaSequence) { 32275 return 0; 32276 } 32277 32278 // do a backward walk to estimate the duration 32279 backward = backwardDuration(playlist, endSequence); 32280 if (backward.precise) { 32281 // if we were able to base our duration estimate on timing 32282 // information provided directly from the Media Source, return 32283 // it 32284 return backward.result; 32285 } 32286 32287 // walk forward to see if a precise duration estimate can be made 32288 // that way 32289 forward = forwardDuration(playlist, endSequence); 32290 if (forward.precise) { 32291 // we found a segment that has been buffered and so it's 32292 // position is known precisely 32293 return forward.result; 32294 } 32295 32296 // return the less-precise, playlist-based duration estimate 32297 return backward.result + expired; 32298 }; 32299 32300 /** 32301 * Calculates the duration of a playlist. If a start and end index 32302 * are specified, the duration will be for the subset of the media 32303 * timeline between those two indices. The total duration for live 32304 * playlists is always Infinity. 32305 * 32306 * @param {Object} playlist a media playlist object 32307 * @param {Number=} endSequence an exclusive upper 32308 * boundary for the playlist. Defaults to the playlist media 32309 * sequence number plus its length. 32310 * @param {Number=} expired the amount of time that has 32311 * dropped off the front of the playlist in a live scenario 32312 * @return {Number} the duration between the start index and end 32313 * index. 32314 */ 32315 var duration = function duration(playlist, endSequence, expired) { 32316 if (!playlist) { 32317 return 0; 32318 } 32319 32320 if (typeof expired !== 'number') { 32321 expired = 0; 32322 } 32323 32324 // if a slice of the total duration is not requested, use 32325 // playlist-level duration indicators when they're present 32326 if (typeof endSequence === 'undefined') { 32327 // if present, use the duration specified in the playlist 32328 if (playlist.totalDuration) { 32329 return playlist.totalDuration; 32330 } 32331 32332 // duration should be Infinity for live playlists 32333 if (!playlist.endList) { 32334 return window_1.Infinity; 32335 } 32336 } 32337 32338 // calculate the total duration based on the segment durations 32339 return intervalDuration(playlist, endSequence, expired); 32340 }; 32341 32342 /** 32343 * Calculate the time between two indexes in the current playlist 32344 * neight the start- nor the end-index need to be within the current 32345 * playlist in which case, the targetDuration of the playlist is used 32346 * to approximate the durations of the segments 32347 * 32348 * @param {Object} playlist a media playlist object 32349 * @param {Number} startIndex 32350 * @param {Number} endIndex 32351 * @return {Number} the number of seconds between startIndex and endIndex 32352 */ 32353 var sumDurations = function sumDurations(playlist, startIndex, endIndex) { 32354 var durations = 0; 32355 32356 if (startIndex > endIndex) { 32357 var _ref = [endIndex, startIndex]; 32358 startIndex = _ref[0]; 32359 endIndex = _ref[1]; 32360 } 32361 32362 if (startIndex < 0) { 32363 for (var i = startIndex; i < Math.min(0, endIndex); i++) { 32364 durations += playlist.targetDuration; 32365 } 32366 startIndex = 0; 32367 } 32368 32369 for (var _i = startIndex; _i < endIndex; _i++) { 32370 durations += playlist.segments[_i].duration; 32371 } 32372 32373 return durations; 32374 }; 32375 32376 /** 32377 * Determines the media index of the segment corresponding to the safe edge of the live 32378 * window which is the duration of the last segment plus 2 target durations from the end 32379 * of the playlist. 32380 * 32381 * @param {Object} playlist 32382 * a media playlist object 32383 * @return {Number} 32384 * The media index of the segment at the safe live point. 0 if there is no "safe" 32385 * point. 32386 * @function safeLiveIndex 32387 */ 32388 var safeLiveIndex = function safeLiveIndex(playlist) { 32389 if (!playlist.segments.length) { 32390 return 0; 32391 } 32392 32393 var i = playlist.segments.length - 1; 32394 var distanceFromEnd = playlist.segments[i].duration || playlist.targetDuration; 32395 var safeDistance = distanceFromEnd + playlist.targetDuration * 2; 32396 32397 while (i--) { 32398 distanceFromEnd += playlist.segments[i].duration; 32399 32400 if (distanceFromEnd >= safeDistance) { 32401 break; 32402 } 32403 } 32404 32405 return Math.max(0, i); 32406 }; 32407 32408 /** 32409 * Calculates the playlist end time 32410 * 32411 * @param {Object} playlist a media playlist object 32412 * @param {Number=} expired the amount of time that has 32413 * dropped off the front of the playlist in a live scenario 32414 * @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 32415 * playlist end calculation should consider the safe live end 32416 * (truncate the playlist end by three segments). This is normally 32417 * used for calculating the end of the playlist's seekable range. 32418 * @returns {Number} the end time of playlist 32419 * @function playlistEnd 32420 */ 32421 var playlistEnd = function playlistEnd(playlist, expired, useSafeLiveEnd) { 32422 if (!playlist || !playlist.segments) { 32423 return null; 32424 } 32425 if (playlist.endList) { 32426 return duration(playlist); 32427 } 32428 32429 if (expired === null) { 32430 return null; 32431 } 32432 32433 expired = expired || 0; 32434 32435 var endSequence = useSafeLiveEnd ? safeLiveIndex(playlist) : playlist.segments.length; 32436 32437 return intervalDuration(playlist, playlist.mediaSequence + endSequence, expired); 32438 }; 32439 32440 /** 32441 * Calculates the interval of time that is currently seekable in a 32442 * playlist. The returned time ranges are relative to the earliest 32443 * moment in the specified playlist that is still available. A full 32444 * seekable implementation for live streams would need to offset 32445 * these values by the duration of content that has expired from the 32446 * stream. 32447 * 32448 * @param {Object} playlist a media playlist object 32449 * dropped off the front of the playlist in a live scenario 32450 * @param {Number=} expired the amount of time that has 32451 * dropped off the front of the playlist in a live scenario 32452 * @return {TimeRanges} the periods of time that are valid targets 32453 * for seeking 32454 */ 32455 var seekable = function seekable(playlist, expired) { 32456 var useSafeLiveEnd = true; 32457 var seekableStart = expired || 0; 32458 var seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd); 32459 32460 if (seekableEnd === null) { 32461 return createTimeRange(); 32462 } 32463 return createTimeRange(seekableStart, seekableEnd); 32464 }; 32465 32466 var isWholeNumber = function isWholeNumber(num) { 32467 return num - Math.floor(num) === 0; 32468 }; 32469 32470 var roundSignificantDigit = function roundSignificantDigit(increment, num) { 32471 // If we have a whole number, just add 1 to it 32472 if (isWholeNumber(num)) { 32473 return num + increment * 0.1; 32474 } 32475 32476 var numDecimalDigits = num.toString().split('.')[1].length; 32477 32478 for (var i = 1; i <= numDecimalDigits; i++) { 32479 var scale = Math.pow(10, i); 32480 var temp = num * scale; 32481 32482 if (isWholeNumber(temp) || i === numDecimalDigits) { 32483 return (temp + increment) / scale; 32484 } 32485 } 32486 }; 32487 32488 var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1); 32489 var floorLeastSignificantDigit = roundSignificantDigit.bind(null, -1); 32490 32491 /** 32492 * Determine the index and estimated starting time of the segment that 32493 * contains a specified playback position in a media playlist. 32494 * 32495 * @param {Object} playlist the media playlist to query 32496 * @param {Number} currentTime The number of seconds since the earliest 32497 * possible position to determine the containing segment for 32498 * @param {Number} startIndex 32499 * @param {Number} startTime 32500 * @return {Object} 32501 */ 32502 var getMediaInfoForTime = function getMediaInfoForTime(playlist, currentTime, startIndex, startTime) { 32503 var i = void 0; 32504 var segment = void 0; 32505 var numSegments = playlist.segments.length; 32506 32507 var time = currentTime - startTime; 32508 32509 if (time < 0) { 32510 // Walk backward from startIndex in the playlist, adding durations 32511 // until we find a segment that contains `time` and return it 32512 if (startIndex > 0) { 32513 for (i = startIndex - 1; i >= 0; i--) { 32514 segment = playlist.segments[i]; 32515 time += floorLeastSignificantDigit(segment.duration); 32516 if (time > 0) { 32517 return { 32518 mediaIndex: i, 32519 startTime: startTime - sumDurations(playlist, startIndex, i) 32520 }; 32521 } 32522 } 32523 } 32524 // We were unable to find a good segment within the playlist 32525 // so select the first segment 32526 return { 32527 mediaIndex: 0, 32528 startTime: currentTime 32529 }; 32530 } 32531 32532 // When startIndex is negative, we first walk forward to first segment 32533 // adding target durations. If we "run out of time" before getting to 32534 // the first segment, return the first segment 32535 if (startIndex < 0) { 32536 for (i = startIndex; i < 0; i++) { 32537 time -= playlist.targetDuration; 32538 if (time < 0) { 32539 return { 32540 mediaIndex: 0, 32541 startTime: currentTime 32542 }; 32543 } 32544 } 32545 startIndex = 0; 32546 } 32547 32548 // Walk forward from startIndex in the playlist, subtracting durations 32549 // until we find a segment that contains `time` and return it 32550 for (i = startIndex; i < numSegments; i++) { 32551 segment = playlist.segments[i]; 32552 time -= ceilLeastSignificantDigit(segment.duration); 32553 if (time < 0) { 32554 return { 32555 mediaIndex: i, 32556 startTime: startTime + sumDurations(playlist, startIndex, i) 32557 }; 32558 } 32559 } 32560 32561 // We are out of possible candidates so load the last one... 32562 return { 32563 mediaIndex: numSegments - 1, 32564 startTime: currentTime 32565 }; 32566 }; 32567 32568 /** 32569 * Check whether the playlist is blacklisted or not. 32570 * 32571 * @param {Object} playlist the media playlist object 32572 * @return {boolean} whether the playlist is blacklisted or not 32573 * @function isBlacklisted 32574 */ 32575 var isBlacklisted = function isBlacklisted(playlist) { 32576 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 32577 }; 32578 32579 /** 32580 * Check whether the playlist is compatible with current playback configuration or has 32581 * been blacklisted permanently for being incompatible. 32582 * 32583 * @param {Object} playlist the media playlist object 32584 * @return {boolean} whether the playlist is incompatible or not 32585 * @function isIncompatible 32586 */ 32587 var isIncompatible = function isIncompatible(playlist) { 32588 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 32589 }; 32590 32591 /** 32592 * Check whether the playlist is enabled or not. 32593 * 32594 * @param {Object} playlist the media playlist object 32595 * @return {boolean} whether the playlist is enabled or not 32596 * @function isEnabled 32597 */ 32598 var isEnabled = function isEnabled(playlist) { 32599 var blacklisted = isBlacklisted(playlist); 32600 32601 return !playlist.disabled && !blacklisted; 32602 }; 32603 32604 /** 32605 * Check whether the playlist has been manually disabled through the representations api. 32606 * 32607 * @param {Object} playlist the media playlist object 32608 * @return {boolean} whether the playlist is disabled manually or not 32609 * @function isDisabled 32610 */ 32611 var isDisabled = function isDisabled(playlist) { 32612 return playlist.disabled; 32613 }; 32614 32615 /** 32616 * Returns whether the current playlist is an AES encrypted HLS stream 32617 * 32618 * @return {Boolean} true if it's an AES encrypted HLS stream 32619 */ 32620 var isAes = function isAes(media) { 32621 for (var i = 0; i < media.segments.length; i++) { 32622 if (media.segments[i].key) { 32623 return true; 32624 } 32625 } 32626 return false; 32627 }; 32628 32629 /** 32630 * Returns whether the current playlist contains fMP4 32631 * 32632 * @return {Boolean} true if the playlist contains fMP4 32633 */ 32634 var isFmp4 = function isFmp4(media) { 32635 for (var i = 0; i < media.segments.length; i++) { 32636 if (media.segments[i].map) { 32637 return true; 32638 } 32639 } 32640 return false; 32641 }; 32642 32643 /** 32644 * Checks if the playlist has a value for the specified attribute 32645 * 32646 * @param {String} attr 32647 * Attribute to check for 32648 * @param {Object} playlist 32649 * The media playlist object 32650 * @return {Boolean} 32651 * Whether the playlist contains a value for the attribute or not 32652 * @function hasAttribute 32653 */ 32654 var hasAttribute = function hasAttribute(attr, playlist) { 32655 return playlist.attributes && playlist.attributes[attr]; 32656 }; 32657 32658 /** 32659 * Estimates the time required to complete a segment download from the specified playlist 32660 * 32661 * @param {Number} segmentDuration 32662 * Duration of requested segment 32663 * @param {Number} bandwidth 32664 * Current measured bandwidth of the player 32665 * @param {Object} playlist 32666 * The media playlist object 32667 * @param {Number=} bytesReceived 32668 * Number of bytes already received for the request. Defaults to 0 32669 * @return {Number|NaN} 32670 * The estimated time to request the segment. NaN if bandwidth information for 32671 * the given playlist is unavailable 32672 * @function estimateSegmentRequestTime 32673 */ 32674 var estimateSegmentRequestTime = function estimateSegmentRequestTime(segmentDuration, bandwidth, playlist) { 32675 var bytesReceived = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; 32676 32677 if (!hasAttribute('BANDWIDTH', playlist)) { 32678 return NaN; 32679 } 32680 32681 var size = segmentDuration * playlist.attributes.BANDWIDTH; 32682 32683 return (size - bytesReceived * 8) / bandwidth; 32684 }; 32685 32686 /* 32687 * Returns whether the current playlist is the lowest rendition 32688 * 32689 * @return {Boolean} true if on lowest rendition 32690 */ 32691 var isLowestEnabledRendition = function isLowestEnabledRendition(master, media) { 32692 if (master.playlists.length === 1) { 32693 return true; 32694 } 32695 32696 var currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 32697 32698 return master.playlists.filter(function (playlist) { 32699 if (!isEnabled(playlist)) { 32700 return false; 32701 } 32702 32703 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 32704 }).length === 0; 32705 }; 32706 32707 // exports 32708 var Playlist = { 32709 duration: duration, 32710 seekable: seekable, 32711 safeLiveIndex: safeLiveIndex, 32712 getMediaInfoForTime: getMediaInfoForTime, 32713 isEnabled: isEnabled, 32714 isDisabled: isDisabled, 32715 isBlacklisted: isBlacklisted, 32716 isIncompatible: isIncompatible, 32717 playlistEnd: playlistEnd, 32718 isAes: isAes, 32719 isFmp4: isFmp4, 32720 hasAttribute: hasAttribute, 32721 estimateSegmentRequestTime: estimateSegmentRequestTime, 32722 isLowestEnabledRendition: isLowestEnabledRendition 32723 }; 32724 32725 /** 32726 * @file xhr.js 32727 */ 32728 32729 var videojsXHR = videojs$1.xhr, 32730 mergeOptions$1$1 = videojs$1.mergeOptions; 32731 32732 var xhrFactory = function xhrFactory() { 32733 var xhr = function XhrFunction(options, callback) { 32734 // Add a default timeout for all hls requests 32735 options = mergeOptions$1$1({ 32736 timeout: 45e3 32737 }, options); 32738 32739 // Allow an optional user-specified function to modify the option 32740 // object before we construct the xhr request 32741 var beforeRequest = XhrFunction.beforeRequest || videojs$1.Hls.xhr.beforeRequest; 32742 32743 if (beforeRequest && typeof beforeRequest === 'function') { 32744 var newOptions = beforeRequest(options); 32745 32746 if (newOptions) { 32747 options = newOptions; 32748 } 32749 } 32750 32751 var request = videojsXHR(options, function (error, response) { 32752 var reqResponse = request.response; 32753 32754 if (!error && reqResponse) { 32755 request.responseTime = Date.now(); 32756 request.roundTripTime = request.responseTime - request.requestTime; 32757 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 32758 if (!request.bandwidth) { 32759 request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000); 32760 } 32761 } 32762 32763 if (response.headers) { 32764 request.responseHeaders = response.headers; 32765 } 32766 32767 // videojs.xhr now uses a specific code on the error 32768 // object to signal that a request has timed out instead 32769 // of setting a boolean on the request object 32770 if (error && error.code === 'ETIMEDOUT') { 32771 request.timedout = true; 32772 } 32773 32774 // videojs.xhr no longer considers status codes outside of 200 and 0 32775 // (for file uris) to be errors, but the old XHR did, so emulate that 32776 // behavior. Status 206 may be used in response to byterange requests. 32777 if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) { 32778 error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText))); 32779 } 32780 32781 callback(error, request); 32782 }); 32783 var originalAbort = request.abort; 32784 32785 request.abort = function () { 32786 request.aborted = true; 32787 return originalAbort.apply(request, arguments); 32788 }; 32789 request.uri = options.uri; 32790 request.requestTime = Date.now(); 32791 return request; 32792 }; 32793 32794 return xhr; 32795 }; 32796 32797 /** 32798 * @file bin-utils.js 32799 */ 32800 32801 /** 32802 * convert a TimeRange to text 32803 * 32804 * @param {TimeRange} range the timerange to use for conversion 32805 * @param {Number} i the iterator on the range to convert 32806 */ 32807 var textRange = function textRange(range, i) { 32808 return range.start(i) + '-' + range.end(i); 32809 }; 32810 32811 /** 32812 * format a number as hex string 32813 * 32814 * @param {Number} e The number 32815 * @param {Number} i the iterator 32816 */ 32817 var formatHexString = function formatHexString(e, i) { 32818 var value = e.toString(16); 32819 32820 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 32821 }; 32822 var formatAsciiString = function formatAsciiString(e) { 32823 if (e >= 0x20 && e < 0x7e) { 32824 return String.fromCharCode(e); 32825 } 32826 return '.'; 32827 }; 32828 32829 /** 32830 * Creates an object for sending to a web worker modifying properties that are TypedArrays 32831 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 32832 * 32833 * @param {Object} message 32834 * Object of properties and values to send to the web worker 32835 * @return {Object} 32836 * Modified message with TypedArray values expanded 32837 * @function createTransferableMessage 32838 */ 32839 var createTransferableMessage = function createTransferableMessage(message) { 32840 var transferable = {}; 32841 32842 Object.keys(message).forEach(function (key) { 32843 var value = message[key]; 32844 32845 if (ArrayBuffer.isView(value)) { 32846 transferable[key] = { 32847 bytes: value.buffer, 32848 byteOffset: value.byteOffset, 32849 byteLength: value.byteLength 32850 }; 32851 } else { 32852 transferable[key] = value; 32853 } 32854 }); 32855 32856 return transferable; 32857 }; 32858 32859 /** 32860 * Returns a unique string identifier for a media initialization 32861 * segment. 32862 */ 32863 var initSegmentId = function initSegmentId(initSegment) { 32864 var byterange = initSegment.byterange || { 32865 length: Infinity, 32866 offset: 0 32867 }; 32868 32869 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 32870 }; 32871 32872 /** 32873 * utils to help dump binary data to the console 32874 */ 32875 var hexDump = function hexDump(data) { 32876 var bytes = Array.prototype.slice.call(data); 32877 var step = 16; 32878 var result = ''; 32879 var hex = void 0; 32880 var ascii = void 0; 32881 32882 for (var j = 0; j < bytes.length / step; j++) { 32883 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 32884 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 32885 result += hex + ' ' + ascii + '\n'; 32886 } 32887 32888 return result; 32889 }; 32890 32891 var tagDump = function tagDump(_ref) { 32892 var bytes = _ref.bytes; 32893 return hexDump(bytes); 32894 }; 32895 32896 var textRanges = function textRanges(ranges) { 32897 var result = ''; 32898 var i = void 0; 32899 32900 for (i = 0; i < ranges.length; i++) { 32901 result += textRange(ranges, i) + ' '; 32902 } 32903 return result; 32904 }; 32905 32906 var utils = /*#__PURE__*/Object.freeze({ 32907 createTransferableMessage: createTransferableMessage, 32908 initSegmentId: initSegmentId, 32909 hexDump: hexDump, 32910 tagDump: tagDump, 32911 textRanges: textRanges 32912 }); 32913 32914 /** 32915 * ranges 32916 * 32917 * Utilities for working with TimeRanges. 32918 * 32919 */ 32920 32921 // Fudge factor to account for TimeRanges rounding 32922 var TIME_FUDGE_FACTOR = 1 / 30; 32923 // Comparisons between time values such as current time and the end of the buffered range 32924 // can be misleading because of precision differences or when the current media has poorly 32925 // aligned audio and video, which can cause values to be slightly off from what you would 32926 // expect. This value is what we consider to be safe to use in such comparisons to account 32927 // for these scenarios. 32928 var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 32929 var filterRanges = function filterRanges(timeRanges, predicate) { 32930 var results = []; 32931 var i = void 0; 32932 32933 if (timeRanges && timeRanges.length) { 32934 // Search for ranges that match the predicate 32935 for (i = 0; i < timeRanges.length; i++) { 32936 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 32937 results.push([timeRanges.start(i), timeRanges.end(i)]); 32938 } 32939 } 32940 } 32941 32942 return videojs$1.createTimeRanges(results); 32943 }; 32944 32945 /** 32946 * Attempts to find the buffered TimeRange that contains the specified 32947 * time. 32948 * @param {TimeRanges} buffered - the TimeRanges object to query 32949 * @param {number} time - the time to filter on. 32950 * @returns {TimeRanges} a new TimeRanges object 32951 */ 32952 var findRange = function findRange(buffered, time) { 32953 return filterRanges(buffered, function (start, end) { 32954 return start - TIME_FUDGE_FACTOR <= time && end + TIME_FUDGE_FACTOR >= time; 32955 }); 32956 }; 32957 32958 /** 32959 * Returns the TimeRanges that begin later than the specified time. 32960 * @param {TimeRanges} timeRanges - the TimeRanges object to query 32961 * @param {number} time - the time to filter on. 32962 * @returns {TimeRanges} a new TimeRanges object. 32963 */ 32964 var findNextRange = function findNextRange(timeRanges, time) { 32965 return filterRanges(timeRanges, function (start) { 32966 return start - TIME_FUDGE_FACTOR >= time; 32967 }); 32968 }; 32969 32970 /** 32971 * Returns gaps within a list of TimeRanges 32972 * @param {TimeRanges} buffered - the TimeRanges object 32973 * @return {TimeRanges} a TimeRanges object of gaps 32974 */ 32975 var findGaps = function findGaps(buffered) { 32976 if (buffered.length < 2) { 32977 return videojs$1.createTimeRanges(); 32978 } 32979 32980 var ranges = []; 32981 32982 for (var i = 1; i < buffered.length; i++) { 32983 var start = buffered.end(i - 1); 32984 var end = buffered.start(i); 32985 32986 ranges.push([start, end]); 32987 } 32988 32989 return videojs$1.createTimeRanges(ranges); 32990 }; 32991 32992 /** 32993 * Gets a human readable string for a TimeRange 32994 * 32995 * @param {TimeRange} range 32996 * @returns {String} a human readable string 32997 */ 32998 var printableRange = function printableRange(range) { 32999 var strArr = []; 33000 33001 if (!range || !range.length) { 33002 return ''; 33003 } 33004 33005 for (var i = 0; i < range.length; i++) { 33006 strArr.push(range.start(i) + ' => ' + range.end(i)); 33007 } 33008 33009 return strArr.join(', '); 33010 }; 33011 33012 /** 33013 * Calculates the amount of time left in seconds until the player hits the end of the 33014 * buffer and causes a rebuffer 33015 * 33016 * @param {TimeRange} buffered 33017 * The state of the buffer 33018 * @param {Numnber} currentTime 33019 * The current time of the player 33020 * @param {Number} playbackRate 33021 * The current playback rate of the player. Defaults to 1. 33022 * @return {Number} 33023 * Time until the player has to start rebuffering in seconds. 33024 * @function timeUntilRebuffer 33025 */ 33026 var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime) { 33027 var playbackRate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; 33028 33029 var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 33030 33031 return (bufferedEnd - currentTime) / playbackRate; 33032 }; 33033 33034 /** 33035 * Converts a TimeRanges object into an array representation 33036 * @param {TimeRanges} timeRanges 33037 * @returns {Array} 33038 */ 33039 var timeRangesToArray = function timeRangesToArray(timeRanges) { 33040 var timeRangesList = []; 33041 33042 for (var i = 0; i < timeRanges.length; i++) { 33043 timeRangesList.push({ 33044 start: timeRanges.start(i), 33045 end: timeRanges.end(i) 33046 }); 33047 } 33048 33049 return timeRangesList; 33050 }; 33051 33052 /** 33053 * @file create-text-tracks-if-necessary.js 33054 */ 33055 33056 /** 33057 * Create text tracks on video.js if they exist on a segment. 33058 * 33059 * @param {Object} sourceBuffer the VSB or FSB 33060 * @param {Object} mediaSource the HTML media source 33061 * @param {Object} segment the segment that may contain the text track 33062 * @private 33063 */ 33064 var createTextTracksIfNecessary = function createTextTracksIfNecessary(sourceBuffer, mediaSource, segment) { 33065 var player = mediaSource.player_; 33066 33067 // create an in-band caption track if one is present in the segment 33068 if (segment.captions && segment.captions.length) { 33069 if (!sourceBuffer.inbandTextTracks_) { 33070 sourceBuffer.inbandTextTracks_ = {}; 33071 } 33072 33073 for (var trackId in segment.captionStreams) { 33074 if (!sourceBuffer.inbandTextTracks_[trackId]) { 33075 player.tech_.trigger({ type: 'usage', name: 'hls-608' }); 33076 var track = player.textTracks().getTrackById(trackId); 33077 33078 if (track) { 33079 // Resuse an existing track with a CC# id because this was 33080 // very likely created by videojs-contrib-hls from information 33081 // in the m3u8 for us to use 33082 sourceBuffer.inbandTextTracks_[trackId] = track; 33083 } else { 33084 // Otherwise, create a track with the default `CC#` label and 33085 // without a language 33086 sourceBuffer.inbandTextTracks_[trackId] = player.addRemoteTextTrack({ 33087 kind: 'captions', 33088 id: trackId, 33089 label: trackId 33090 }, false).track; 33091 } 33092 } 33093 } 33094 } 33095 33096 if (segment.metadata && segment.metadata.length && !sourceBuffer.metadataTrack_) { 33097 sourceBuffer.metadataTrack_ = player.addRemoteTextTrack({ 33098 kind: 'metadata', 33099 label: 'Timed Metadata' 33100 }, false).track; 33101 sourceBuffer.metadataTrack_.inBandMetadataTrackDispatchType = segment.metadata.dispatchType; 33102 } 33103 }; 33104 33105 /** 33106 * @file remove-cues-from-track.js 33107 */ 33108 33109 /** 33110 * Remove cues from a track on video.js. 33111 * 33112 * @param {Double} start start of where we should remove the cue 33113 * @param {Double} end end of where the we should remove the cue 33114 * @param {Object} track the text track to remove the cues from 33115 * @private 33116 */ 33117 var removeCuesFromTrack = function removeCuesFromTrack(start, end, track) { 33118 var i = void 0; 33119 var cue = void 0; 33120 33121 if (!track) { 33122 return; 33123 } 33124 33125 if (!track.cues) { 33126 return; 33127 } 33128 33129 i = track.cues.length; 33130 33131 while (i--) { 33132 cue = track.cues[i]; 33133 33134 // Remove any overlapping cue 33135 if (cue.startTime <= end && cue.endTime >= start) { 33136 track.removeCue(cue); 33137 } 33138 } 33139 }; 33140 33141 /** 33142 * @file add-text-track-data.js 33143 */ 33144 /** 33145 * Define properties on a cue for backwards compatability, 33146 * but warn the user that the way that they are using it 33147 * is depricated and will be removed at a later date. 33148 * 33149 * @param {Cue} cue the cue to add the properties on 33150 * @private 33151 */ 33152 var deprecateOldCue = function deprecateOldCue(cue) { 33153 Object.defineProperties(cue.frame, { 33154 id: { 33155 get: function get$$1() { 33156 videojs$1.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 33157 return cue.value.key; 33158 } 33159 }, 33160 value: { 33161 get: function get$$1() { 33162 videojs$1.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 33163 return cue.value.data; 33164 } 33165 }, 33166 privateData: { 33167 get: function get$$1() { 33168 videojs$1.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 33169 return cue.value.data; 33170 } 33171 } 33172 }); 33173 }; 33174 33175 var durationOfVideo = function durationOfVideo(duration) { 33176 var dur = void 0; 33177 33178 if (isNaN(duration) || Math.abs(duration) === Infinity) { 33179 dur = Number.MAX_VALUE; 33180 } else { 33181 dur = duration; 33182 } 33183 return dur; 33184 }; 33185 /** 33186 * Add text track data to a source handler given the captions and 33187 * metadata from the buffer. 33188 * 33189 * @param {Object} sourceHandler the virtual source buffer 33190 * @param {Array} captionArray an array of caption data 33191 * @param {Array} metadataArray an array of meta data 33192 * @private 33193 */ 33194 var addTextTrackData = function addTextTrackData(sourceHandler, captionArray, metadataArray) { 33195 var Cue = window_1.WebKitDataCue || window_1.VTTCue; 33196 33197 if (captionArray) { 33198 captionArray.forEach(function (caption) { 33199 var track = caption.stream; 33200 33201 this.inbandTextTracks_[track].addCue(new Cue(caption.startTime + this.timestampOffset, caption.endTime + this.timestampOffset, caption.text)); 33202 }, sourceHandler); 33203 } 33204 33205 if (metadataArray) { 33206 var videoDuration = durationOfVideo(sourceHandler.mediaSource_.duration); 33207 33208 metadataArray.forEach(function (metadata) { 33209 var time = metadata.cueTime + this.timestampOffset; 33210 33211 metadata.frames.forEach(function (frame) { 33212 var cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 33213 33214 cue.frame = frame; 33215 cue.value = frame; 33216 deprecateOldCue(cue); 33217 33218 this.metadataTrack_.addCue(cue); 33219 }, this); 33220 }, sourceHandler); 33221 33222 // Updating the metadeta cues so that 33223 // the endTime of each cue is the startTime of the next cue 33224 // the endTime of last cue is the duration of the video 33225 if (sourceHandler.metadataTrack_ && sourceHandler.metadataTrack_.cues && sourceHandler.metadataTrack_.cues.length) { 33226 var cues = sourceHandler.metadataTrack_.cues; 33227 var cuesArray = []; 33228 33229 // Create a copy of the TextTrackCueList... 33230 // ...disregarding cues with a falsey value 33231 for (var i = 0; i < cues.length; i++) { 33232 if (cues[i]) { 33233 cuesArray.push(cues[i]); 33234 } 33235 } 33236 33237 // Group cues by their startTime value 33238 var cuesGroupedByStartTime = cuesArray.reduce(function (obj, cue) { 33239 var timeSlot = obj[cue.startTime] || []; 33240 33241 timeSlot.push(cue); 33242 obj[cue.startTime] = timeSlot; 33243 33244 return obj; 33245 }, {}); 33246 33247 // Sort startTimes by ascending order 33248 var sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort(function (a, b) { 33249 return Number(a) - Number(b); 33250 }); 33251 33252 // Map each cue group's endTime to the next group's startTime 33253 sortedStartTimes.forEach(function (startTime, idx) { 33254 var cueGroup = cuesGroupedByStartTime[startTime]; 33255 var nextTime = Number(sortedStartTimes[idx + 1]) || videoDuration; 33256 33257 // Map each cue's endTime the next group's startTime 33258 cueGroup.forEach(function (cue) { 33259 cue.endTime = nextTime; 33260 }); 33261 }); 33262 } 33263 } 33264 }; 33265 33266 var win$1 = typeof window !== 'undefined' ? window : {}, 33267 TARGET = typeof Symbol === 'undefined' ? '__target' : Symbol(), 33268 SCRIPT_TYPE = 'application/javascript', 33269 BlobBuilder = win$1.BlobBuilder || win$1.WebKitBlobBuilder || win$1.MozBlobBuilder || win$1.MSBlobBuilder, 33270 URL = win$1.URL || win$1.webkitURL || URL && URL.msURL, 33271 Worker = win$1.Worker; 33272 33273 /** 33274 * Returns a wrapper around Web Worker code that is constructible. 33275 * 33276 * @function shimWorker 33277 * 33278 * @param { String } filename The name of the file 33279 * @param { Function } fn Function wrapping the code of the worker 33280 */ 33281 function shimWorker(filename, fn) { 33282 return function ShimWorker(forceFallback) { 33283 var o = this; 33284 33285 if (!fn) { 33286 return new Worker(filename); 33287 } else if (Worker && !forceFallback) { 33288 // Convert the function's inner code to a string to construct the worker 33289 var source = fn.toString().replace(/^function.+?{/, '').slice(0, -1), 33290 objURL = createSourceObject(source); 33291 33292 this[TARGET] = new Worker(objURL); 33293 wrapTerminate(this[TARGET], objURL); 33294 return this[TARGET]; 33295 } else { 33296 var selfShim = { 33297 postMessage: function postMessage(m) { 33298 if (o.onmessage) { 33299 setTimeout(function () { 33300 o.onmessage({ data: m, target: selfShim }); 33301 }); 33302 } 33303 } 33304 }; 33305 33306 fn.call(selfShim); 33307 this.postMessage = function (m) { 33308 setTimeout(function () { 33309 selfShim.onmessage({ data: m, target: o }); 33310 }); 33311 }; 33312 this.isThisThread = true; 33313 } 33314 }; 33315 } 33316 // Test Worker capabilities 33317 if (Worker) { 33318 var testWorker, 33319 objURL = createSourceObject('self.onmessage = function () {}'), 33320 testArray = new Uint8Array(1); 33321 33322 try { 33323 testWorker = new Worker(objURL); 33324 33325 // Native browser on some Samsung devices throws for transferables, let's detect it 33326 testWorker.postMessage(testArray, [testArray.buffer]); 33327 } catch (e) { 33328 Worker = null; 33329 } finally { 33330 URL.revokeObjectURL(objURL); 33331 if (testWorker) { 33332 testWorker.terminate(); 33333 } 33334 } 33335 } 33336 33337 function createSourceObject(str) { 33338 try { 33339 return URL.createObjectURL(new Blob([str], { type: SCRIPT_TYPE })); 33340 } catch (e) { 33341 var blob = new BlobBuilder(); 33342 blob.append(str); 33343 return URL.createObjectURL(blob.getBlob(type)); 33344 } 33345 } 33346 33347 function wrapTerminate(worker, objURL) { 33348 if (!worker || !objURL) return; 33349 var term = worker.terminate; 33350 worker.objURL = objURL; 33351 worker.terminate = function () { 33352 if (worker.objURL) URL.revokeObjectURL(worker.objURL); 33353 term.call(worker); 33354 }; 33355 } 33356 33357 var TransmuxWorker = new shimWorker("./transmuxer-worker.worker.js", function (window, document$$1) { 33358 var self = this; 33359 var transmuxerWorker = function () { 33360 33361 var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 33362 33363 var win; 33364 33365 if (typeof window !== "undefined") { 33366 win = window; 33367 } else if (typeof commonjsGlobal !== "undefined") { 33368 win = commonjsGlobal; 33369 } else if (typeof self !== "undefined") { 33370 win = self; 33371 } else { 33372 win = {}; 33373 } 33374 33375 var window_1$$1 = win; 33376 33377 /** 33378 * mux.js 33379 * 33380 * Copyright (c) 2015 Brightcove 33381 * All rights reserved. 33382 * 33383 * Functions that generate fragmented MP4s suitable for use with Media 33384 * Source Extensions. 33385 */ 33386 33387 var UINT32_MAX = Math.pow(2, 32) - 1; 33388 33389 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; 33390 33391 // pre-calculate constants 33392 (function () { 33393 var i; 33394 types = { 33395 avc1: [], // codingname 33396 avcC: [], 33397 btrt: [], 33398 dinf: [], 33399 dref: [], 33400 esds: [], 33401 ftyp: [], 33402 hdlr: [], 33403 mdat: [], 33404 mdhd: [], 33405 mdia: [], 33406 mfhd: [], 33407 minf: [], 33408 moof: [], 33409 moov: [], 33410 mp4a: [], // codingname 33411 mvex: [], 33412 mvhd: [], 33413 sdtp: [], 33414 smhd: [], 33415 stbl: [], 33416 stco: [], 33417 stsc: [], 33418 stsd: [], 33419 stsz: [], 33420 stts: [], 33421 styp: [], 33422 tfdt: [], 33423 tfhd: [], 33424 traf: [], 33425 trak: [], 33426 trun: [], 33427 trex: [], 33428 tkhd: [], 33429 vmhd: [] 33430 }; 33431 33432 // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 33433 // don't throw an error 33434 if (typeof Uint8Array === 'undefined') { 33435 return; 33436 } 33437 33438 for (i in types) { 33439 if (types.hasOwnProperty(i)) { 33440 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 33441 } 33442 } 33443 33444 MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]); 33445 AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]); 33446 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 33447 VIDEO_HDLR = new Uint8Array([0x00, // version 0 33448 0x00, 0x00, 0x00, // flags 33449 0x00, 0x00, 0x00, 0x00, // pre_defined 33450 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 33451 0x00, 0x00, 0x00, 0x00, // reserved 33452 0x00, 0x00, 0x00, 0x00, // reserved 33453 0x00, 0x00, 0x00, 0x00, // reserved 33454 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 33455 ]); 33456 AUDIO_HDLR = new Uint8Array([0x00, // version 0 33457 0x00, 0x00, 0x00, // flags 33458 0x00, 0x00, 0x00, 0x00, // pre_defined 33459 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 33460 0x00, 0x00, 0x00, 0x00, // reserved 33461 0x00, 0x00, 0x00, 0x00, // reserved 33462 0x00, 0x00, 0x00, 0x00, // reserved 33463 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 33464 ]); 33465 HDLR_TYPES = { 33466 video: VIDEO_HDLR, 33467 audio: AUDIO_HDLR 33468 }; 33469 DREF = new Uint8Array([0x00, // version 0 33470 0x00, 0x00, 0x00, // flags 33471 0x00, 0x00, 0x00, 0x01, // entry_count 33472 0x00, 0x00, 0x00, 0x0c, // entry_size 33473 0x75, 0x72, 0x6c, 0x20, // 'url' type 33474 0x00, // version 0 33475 0x00, 0x00, 0x01 // entry_flags 33476 ]); 33477 SMHD = new Uint8Array([0x00, // version 33478 0x00, 0x00, 0x00, // flags 33479 0x00, 0x00, // balance, 0 means centered 33480 0x00, 0x00 // reserved 33481 ]); 33482 STCO = new Uint8Array([0x00, // version 33483 0x00, 0x00, 0x00, // flags 33484 0x00, 0x00, 0x00, 0x00 // entry_count 33485 ]); 33486 STSC = STCO; 33487 STSZ = new Uint8Array([0x00, // version 33488 0x00, 0x00, 0x00, // flags 33489 0x00, 0x00, 0x00, 0x00, // sample_size 33490 0x00, 0x00, 0x00, 0x00 // sample_count 33491 ]); 33492 STTS = STCO; 33493 VMHD = new Uint8Array([0x00, // version 33494 0x00, 0x00, 0x01, // flags 33495 0x00, 0x00, // graphicsmode 33496 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 33497 ]); 33498 })(); 33499 33500 box = function box(type) { 33501 var payload = [], 33502 size = 0, 33503 i, 33504 result, 33505 view; 33506 33507 for (i = 1; i < arguments.length; i++) { 33508 payload.push(arguments[i]); 33509 } 33510 33511 i = payload.length; 33512 33513 // calculate the total size we need to allocate 33514 while (i--) { 33515 size += payload[i].byteLength; 33516 } 33517 result = new Uint8Array(size + 8); 33518 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 33519 view.setUint32(0, result.byteLength); 33520 result.set(type, 4); 33521 33522 // copy the payload into the result 33523 for (i = 0, size = 8; i < payload.length; i++) { 33524 result.set(payload[i], size); 33525 size += payload[i].byteLength; 33526 } 33527 return result; 33528 }; 33529 33530 dinf = function dinf() { 33531 return box(types.dinf, box(types.dref, DREF)); 33532 }; 33533 33534 esds = function esds(track) { 33535 return box(types.esds, new Uint8Array([0x00, // version 33536 0x00, 0x00, 0x00, // flags 33537 33538 // ES_Descriptor 33539 0x03, // tag, ES_DescrTag 33540 0x19, // length 33541 0x00, 0x00, // ES_ID 33542 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority 33543 33544 // DecoderConfigDescriptor 33545 0x04, // tag, DecoderConfigDescrTag 33546 0x11, // length 33547 0x40, // object type 33548 0x15, // streamType 33549 0x00, 0x06, 0x00, // bufferSizeDB 33550 0x00, 0x00, 0xda, 0xc0, // maxBitrate 33551 0x00, 0x00, 0xda, 0xc0, // avgBitrate 33552 33553 // DecoderSpecificInfo 33554 0x05, // tag, DecoderSpecificInfoTag 33555 0x02, // length 33556 // ISO/IEC 14496-3, AudioSpecificConfig 33557 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 33558 track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig 33559 ])); 33560 }; 33561 33562 ftyp = function ftyp() { 33563 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 33564 }; 33565 33566 hdlr = function hdlr(type) { 33567 return box(types.hdlr, HDLR_TYPES[type]); 33568 }; 33569 mdat = function mdat(data) { 33570 return box(types.mdat, data); 33571 }; 33572 mdhd = function mdhd(track) { 33573 var result = new Uint8Array([0x00, // version 0 33574 0x00, 0x00, 0x00, // flags 33575 0x00, 0x00, 0x00, 0x02, // creation_time 33576 0x00, 0x00, 0x00, 0x03, // modification_time 33577 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 33578 33579 track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration 33580 0x55, 0xc4, // 'und' language (undetermined) 33581 0x00, 0x00]); 33582 33583 // Use the sample rate from the track metadata, when it is 33584 // defined. The sample rate can be parsed out of an ADTS header, for 33585 // instance. 33586 if (track.samplerate) { 33587 result[12] = track.samplerate >>> 24 & 0xFF; 33588 result[13] = track.samplerate >>> 16 & 0xFF; 33589 result[14] = track.samplerate >>> 8 & 0xFF; 33590 result[15] = track.samplerate & 0xFF; 33591 } 33592 33593 return box(types.mdhd, result); 33594 }; 33595 mdia = function mdia(track) { 33596 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 33597 }; 33598 mfhd = function mfhd(sequenceNumber) { 33599 return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags 33600 (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number 33601 ])); 33602 }; 33603 minf = function minf(track) { 33604 return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track)); 33605 }; 33606 moof = function moof(sequenceNumber, tracks) { 33607 var trackFragments = [], 33608 i = tracks.length; 33609 // build traf boxes for each track fragment 33610 while (i--) { 33611 trackFragments[i] = traf(tracks[i]); 33612 } 33613 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 33614 }; 33615 /** 33616 * Returns a movie box. 33617 * @param tracks {array} the tracks associated with this movie 33618 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 33619 */ 33620 moov = function moov(tracks) { 33621 var i = tracks.length, 33622 boxes = []; 33623 33624 while (i--) { 33625 boxes[i] = trak(tracks[i]); 33626 } 33627 33628 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 33629 }; 33630 mvex = function mvex(tracks) { 33631 var i = tracks.length, 33632 boxes = []; 33633 33634 while (i--) { 33635 boxes[i] = trex(tracks[i]); 33636 } 33637 return box.apply(null, [types.mvex].concat(boxes)); 33638 }; 33639 mvhd = function mvhd(duration) { 33640 var bytes = new Uint8Array([0x00, // version 0 33641 0x00, 0x00, 0x00, // flags 33642 0x00, 0x00, 0x00, 0x01, // creation_time 33643 0x00, 0x00, 0x00, 0x02, // modification_time 33644 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 33645 (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration 33646 0x00, 0x01, 0x00, 0x00, // 1.0 rate 33647 0x01, 0x00, // 1.0 volume 33648 0x00, 0x00, // reserved 33649 0x00, 0x00, 0x00, 0x00, // reserved 33650 0x00, 0x00, 0x00, 0x00, // reserved 33651 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 33652 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 33653 0xff, 0xff, 0xff, 0xff // next_track_ID 33654 ]); 33655 return box(types.mvhd, bytes); 33656 }; 33657 33658 sdtp = function sdtp(track) { 33659 var samples = track.samples || [], 33660 bytes = new Uint8Array(4 + samples.length), 33661 flags, 33662 i; 33663 33664 // leave the full box header (4 bytes) all zero 33665 33666 // write the sample table 33667 for (i = 0; i < samples.length; i++) { 33668 flags = samples[i].flags; 33669 33670 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 33671 } 33672 33673 return box(types.sdtp, bytes); 33674 }; 33675 33676 stbl = function stbl(track) { 33677 return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO)); 33678 }; 33679 33680 (function () { 33681 var videoSample, audioSample; 33682 33683 stsd = function stsd(track) { 33684 33685 return box(types.stsd, new Uint8Array([0x00, // version 0 33686 0x00, 0x00, 0x00, // flags 33687 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track)); 33688 }; 33689 33690 videoSample = function videoSample(track) { 33691 var sps = track.sps || [], 33692 pps = track.pps || [], 33693 sequenceParameterSets = [], 33694 pictureParameterSets = [], 33695 i; 33696 33697 // assemble the SPSs 33698 for (i = 0; i < sps.length; i++) { 33699 sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 33700 sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength 33701 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 33702 } 33703 33704 // assemble the PPSs 33705 for (i = 0; i < pps.length; i++) { 33706 pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 33707 pictureParameterSets.push(pps[i].byteLength & 0xFF); 33708 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 33709 } 33710 33711 return box(types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 33712 0x00, 0x01, // data_reference_index 33713 0x00, 0x00, // pre_defined 33714 0x00, 0x00, // reserved 33715 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined 33716 (track.width & 0xff00) >> 8, track.width & 0xff, // width 33717 (track.height & 0xff00) >> 8, track.height & 0xff, // height 33718 0x00, 0x48, 0x00, 0x00, // horizresolution 33719 0x00, 0x48, 0x00, 0x00, // vertresolution 33720 0x00, 0x00, 0x00, 0x00, // reserved 33721 0x00, 0x01, // frame_count 33722 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 33723 0x00, 0x18, // depth = 24 33724 0x11, 0x11 // pre_defined = -1 33725 ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion 33726 track.profileIdc, // AVCProfileIndication 33727 track.profileCompatibility, // profile_compatibility 33728 track.levelIdc, // AVCLevelIndication 33729 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 33730 ].concat([sps.length // numOfSequenceParameterSets 33731 ]).concat(sequenceParameterSets).concat([pps.length // numOfPictureParameterSets 33732 ]).concat(pictureParameterSets))), // "PPS" 33733 box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 33734 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 33735 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate 33736 ); 33737 }; 33738 33739 audioSample = function audioSample(track) { 33740 return box(types.mp4a, new Uint8Array([ 33741 33742 // SampleEntry, ISO/IEC 14496-12 33743 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 33744 0x00, 0x01, // data_reference_index 33745 33746 // AudioSampleEntry, ISO/IEC 14496-12 33747 0x00, 0x00, 0x00, 0x00, // reserved 33748 0x00, 0x00, 0x00, 0x00, // reserved 33749 (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount 33750 33751 (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize 33752 0x00, 0x00, // pre_defined 33753 0x00, 0x00, // reserved 33754 33755 (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16 33756 33757 // MP4AudioSampleEntry, ISO/IEC 14496-14 33758 ]), esds(track)); 33759 }; 33760 })(); 33761 33762 tkhd = function tkhd(track) { 33763 var result = new Uint8Array([0x00, // version 0 33764 0x00, 0x00, 0x07, // flags 33765 0x00, 0x00, 0x00, 0x00, // creation_time 33766 0x00, 0x00, 0x00, 0x00, // modification_time 33767 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 33768 0x00, 0x00, 0x00, 0x00, // reserved 33769 (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration 33770 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 33771 0x00, 0x00, // layer 33772 0x00, 0x00, // alternate_group 33773 0x01, 0x00, // non-audio track volume 33774 0x00, 0x00, // reserved 33775 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 33776 (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width 33777 (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height 33778 ]); 33779 33780 return box(types.tkhd, result); 33781 }; 33782 33783 /** 33784 * Generate a track fragment (traf) box. A traf box collects metadata 33785 * about tracks in a movie fragment (moof) box. 33786 */ 33787 traf = function traf(track) { 33788 var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime; 33789 33790 trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0 33791 0x00, 0x00, 0x3a, // flags 33792 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 33793 0x00, 0x00, 0x00, 0x01, // sample_description_index 33794 0x00, 0x00, 0x00, 0x00, // default_sample_duration 33795 0x00, 0x00, 0x00, 0x00, // default_sample_size 33796 0x00, 0x00, 0x00, 0x00 // default_sample_flags 33797 ])); 33798 33799 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / (UINT32_MAX + 1)); 33800 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % (UINT32_MAX + 1)); 33801 33802 trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1 33803 0x00, 0x00, 0x00, // flags 33804 // baseMediaDecodeTime 33805 upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF])); 33806 33807 // the data offset specifies the number of bytes from the start of 33808 // the containing moof to the first payload byte of the associated 33809 // mdat 33810 dataOffset = 32 + // tfhd 33811 20 + // tfdt 33812 8 + // traf header 33813 16 + // mfhd 33814 8 + // moof header 33815 8; // mdat header 33816 33817 // audio tracks require less metadata 33818 if (track.type === 'audio') { 33819 trackFragmentRun = trun(track, dataOffset); 33820 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 33821 } 33822 33823 // video tracks should contain an independent and disposable samples 33824 // box (sdtp) 33825 // generate one and adjust offsets to match 33826 sampleDependencyTable = sdtp(track); 33827 trackFragmentRun = trun(track, sampleDependencyTable.length + dataOffset); 33828 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable); 33829 }; 33830 33831 /** 33832 * Generate a track box. 33833 * @param track {object} a track definition 33834 * @return {Uint8Array} the track box 33835 */ 33836 trak = function trak(track) { 33837 track.duration = track.duration || 0xffffffff; 33838 return box(types.trak, tkhd(track), mdia(track)); 33839 }; 33840 33841 trex = function trex(track) { 33842 var result = new Uint8Array([0x00, // version 0 33843 0x00, 0x00, 0x00, // flags 33844 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 33845 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 33846 0x00, 0x00, 0x00, 0x00, // default_sample_duration 33847 0x00, 0x00, 0x00, 0x00, // default_sample_size 33848 0x00, 0x01, 0x00, 0x01 // default_sample_flags 33849 ]); 33850 // the last two bytes of default_sample_flags is the sample 33851 // degradation priority, a hint about the importance of this sample 33852 // relative to others. Lower the degradation priority for all sample 33853 // types other than video. 33854 if (track.type !== 'video') { 33855 result[result.length - 1] = 0x00; 33856 } 33857 33858 return box(types.trex, result); 33859 }; 33860 33861 (function () { 33862 var audioTrun, videoTrun, trunHeader; 33863 33864 // This method assumes all samples are uniform. That is, if a 33865 // duration is present for the first sample, it will be present for 33866 // all subsequent samples. 33867 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 33868 trunHeader = function trunHeader(samples, offset) { 33869 var durationPresent = 0, 33870 sizePresent = 0, 33871 flagsPresent = 0, 33872 compositionTimeOffset = 0; 33873 33874 // trun flag constants 33875 if (samples.length) { 33876 if (samples[0].duration !== undefined) { 33877 durationPresent = 0x1; 33878 } 33879 if (samples[0].size !== undefined) { 33880 sizePresent = 0x2; 33881 } 33882 if (samples[0].flags !== undefined) { 33883 flagsPresent = 0x4; 33884 } 33885 if (samples[0].compositionTimeOffset !== undefined) { 33886 compositionTimeOffset = 0x8; 33887 } 33888 } 33889 33890 return [0x00, // version 0 33891 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags 33892 (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count 33893 (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset 33894 ]; 33895 }; 33896 33897 videoTrun = function videoTrun(track, offset) { 33898 var bytes, samples, sample, i; 33899 33900 samples = track.samples || []; 33901 offset += 8 + 12 + 16 * samples.length; 33902 33903 bytes = trunHeader(samples, offset); 33904 33905 for (i = 0; i < samples.length; i++) { 33906 sample = samples[i]; 33907 bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration 33908 (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF, // sample_size 33909 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 33910 (sample.compositionTimeOffset & 0xFF000000) >>> 24, (sample.compositionTimeOffset & 0xFF0000) >>> 16, (sample.compositionTimeOffset & 0xFF00) >>> 8, sample.compositionTimeOffset & 0xFF // sample_composition_time_offset 33911 ]); 33912 } 33913 return box(types.trun, new Uint8Array(bytes)); 33914 }; 33915 33916 audioTrun = function audioTrun(track, offset) { 33917 var bytes, samples, sample, i; 33918 33919 samples = track.samples || []; 33920 offset += 8 + 12 + 8 * samples.length; 33921 33922 bytes = trunHeader(samples, offset); 33923 33924 for (i = 0; i < samples.length; i++) { 33925 sample = samples[i]; 33926 bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration 33927 (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF]); // sample_size 33928 } 33929 33930 return box(types.trun, new Uint8Array(bytes)); 33931 }; 33932 33933 trun = function trun(track, offset) { 33934 if (track.type === 'audio') { 33935 return audioTrun(track, offset); 33936 } 33937 33938 return videoTrun(track, offset); 33939 }; 33940 })(); 33941 33942 var mp4Generator = { 33943 ftyp: ftyp, 33944 mdat: mdat, 33945 moof: moof, 33946 moov: moov, 33947 initSegment: function initSegment(tracks) { 33948 var fileType = ftyp(), 33949 movie = moov(tracks), 33950 result; 33951 33952 result = new Uint8Array(fileType.byteLength + movie.byteLength); 33953 result.set(fileType); 33954 result.set(movie, fileType.byteLength); 33955 return result; 33956 } 33957 }; 33958 33959 /** 33960 * mux.js 33961 * 33962 * Copyright (c) 2014 Brightcove 33963 * All rights reserved. 33964 * 33965 * A lightweight readable stream implemention that handles event dispatching. 33966 * Objects that inherit from streams should call init in their constructors. 33967 */ 33968 33969 var Stream = function Stream() { 33970 this.init = function () { 33971 var listeners = {}; 33972 /** 33973 * Add a listener for a specified event type. 33974 * @param type {string} the event name 33975 * @param listener {function} the callback to be invoked when an event of 33976 * the specified type occurs 33977 */ 33978 this.on = function (type, listener) { 33979 if (!listeners[type]) { 33980 listeners[type] = []; 33981 } 33982 listeners[type] = listeners[type].concat(listener); 33983 }; 33984 /** 33985 * Remove a listener for a specified event type. 33986 * @param type {string} the event name 33987 * @param listener {function} a function previously registered for this 33988 * type of event through `on` 33989 */ 33990 this.off = function (type, listener) { 33991 var index; 33992 if (!listeners[type]) { 33993 return false; 33994 } 33995 index = listeners[type].indexOf(listener); 33996 listeners[type] = listeners[type].slice(); 33997 listeners[type].splice(index, 1); 33998 return index > -1; 33999 }; 34000 /** 34001 * Trigger an event of the specified type on this stream. Any additional 34002 * arguments to this function are passed as parameters to event listeners. 34003 * @param type {string} the event name 34004 */ 34005 this.trigger = function (type) { 34006 var callbacks, i, length, args; 34007 callbacks = listeners[type]; 34008 if (!callbacks) { 34009 return; 34010 } 34011 // Slicing the arguments on every invocation of this method 34012 // can add a significant amount of overhead. Avoid the 34013 // intermediate object creation for the common case of a 34014 // single callback argument 34015 if (arguments.length === 2) { 34016 length = callbacks.length; 34017 for (i = 0; i < length; ++i) { 34018 callbacks[i].call(this, arguments[1]); 34019 } 34020 } else { 34021 args = []; 34022 i = arguments.length; 34023 for (i = 1; i < arguments.length; ++i) { 34024 args.push(arguments[i]); 34025 } 34026 length = callbacks.length; 34027 for (i = 0; i < length; ++i) { 34028 callbacks[i].apply(this, args); 34029 } 34030 } 34031 }; 34032 /** 34033 * Destroys the stream and cleans up. 34034 */ 34035 this.dispose = function () { 34036 listeners = {}; 34037 }; 34038 }; 34039 }; 34040 34041 /** 34042 * Forwards all `data` events on this stream to the destination stream. The 34043 * destination stream should provide a method `push` to receive the data 34044 * events as they arrive. 34045 * @param destination {stream} the stream that will receive all `data` events 34046 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 34047 * when the current stream emits a 'done' event 34048 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 34049 */ 34050 Stream.prototype.pipe = function (destination) { 34051 this.on('data', function (data) { 34052 destination.push(data); 34053 }); 34054 34055 this.on('done', function (flushSource) { 34056 destination.flush(flushSource); 34057 }); 34058 34059 return destination; 34060 }; 34061 34062 // Default stream functions that are expected to be overridden to perform 34063 // actual work. These are provided by the prototype as a sort of no-op 34064 // implementation so that we don't have to check for their existence in the 34065 // `pipe` function above. 34066 Stream.prototype.push = function (data) { 34067 this.trigger('data', data); 34068 }; 34069 34070 Stream.prototype.flush = function (flushSource) { 34071 this.trigger('done', flushSource); 34072 }; 34073 34074 var stream = Stream; 34075 34076 // ----------------- 34077 // Link To Transport 34078 // ----------------- 34079 34080 // Supplemental enhancement information (SEI) NAL units have a 34081 // payload type field to indicate how they are to be 34082 // interpreted. CEAS-708 caption content is always transmitted with 34083 // payload type 0x04. 34084 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 34085 RBSP_TRAILING_BITS = 128; 34086 34087 /** 34088 * Parse a supplemental enhancement information (SEI) NAL unit. 34089 * Stops parsing once a message of type ITU T T35 has been found. 34090 * 34091 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 34092 * @return {object} the parsed SEI payload 34093 * @see Rec. ITU-T H.264, 7.3.2.3.1 34094 */ 34095 var parseSei = function parseSei(bytes) { 34096 var i = 0, 34097 result = { 34098 payloadType: -1, 34099 payloadSize: 0 34100 }, 34101 payloadType = 0, 34102 payloadSize = 0; 34103 34104 // go through the sei_rbsp parsing each each individual sei_message 34105 while (i < bytes.byteLength) { 34106 // stop once we have hit the end of the sei_rbsp 34107 if (bytes[i] === RBSP_TRAILING_BITS) { 34108 break; 34109 } 34110 34111 // Parse payload type 34112 while (bytes[i] === 0xFF) { 34113 payloadType += 255; 34114 i++; 34115 } 34116 payloadType += bytes[i++]; 34117 34118 // Parse payload size 34119 while (bytes[i] === 0xFF) { 34120 payloadSize += 255; 34121 i++; 34122 } 34123 payloadSize += bytes[i++]; 34124 34125 // this sei_message is a 608/708 caption so save it and break 34126 // there can only ever be one caption message in a frame's sei 34127 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 34128 result.payloadType = payloadType; 34129 result.payloadSize = payloadSize; 34130 result.payload = bytes.subarray(i, i + payloadSize); 34131 break; 34132 } 34133 34134 // skip the payload and parse the next message 34135 i += payloadSize; 34136 payloadType = 0; 34137 payloadSize = 0; 34138 } 34139 34140 return result; 34141 }; 34142 34143 // see ANSI/SCTE 128-1 (2013), section 8.1 34144 var parseUserData = function parseUserData(sei) { 34145 // itu_t_t35_contry_code must be 181 (United States) for 34146 // captions 34147 if (sei.payload[0] !== 181) { 34148 return null; 34149 } 34150 34151 // itu_t_t35_provider_code should be 49 (ATSC) for captions 34152 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 34153 return null; 34154 } 34155 34156 // the user_identifier should be "GA94" to indicate ATSC1 data 34157 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 34158 return null; 34159 } 34160 34161 // finally, user_data_type_code should be 0x03 for caption data 34162 if (sei.payload[7] !== 0x03) { 34163 return null; 34164 } 34165 34166 // return the user_data_type_structure and strip the trailing 34167 // marker bits 34168 return sei.payload.subarray(8, sei.payload.length - 1); 34169 }; 34170 34171 // see CEA-708-D, section 4.4 34172 var parseCaptionPackets = function parseCaptionPackets(pts, userData) { 34173 var results = [], 34174 i, 34175 count, 34176 offset, 34177 data; 34178 34179 // if this is just filler, return immediately 34180 if (!(userData[0] & 0x40)) { 34181 return results; 34182 } 34183 34184 // parse out the cc_data_1 and cc_data_2 fields 34185 count = userData[0] & 0x1f; 34186 for (i = 0; i < count; i++) { 34187 offset = i * 3; 34188 data = { 34189 type: userData[offset + 2] & 0x03, 34190 pts: pts 34191 }; 34192 34193 // capture cc data when cc_valid is 1 34194 if (userData[offset + 2] & 0x04) { 34195 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 34196 results.push(data); 34197 } 34198 } 34199 return results; 34200 }; 34201 34202 var CaptionStream = function CaptionStream() { 34203 34204 CaptionStream.prototype.init.call(this); 34205 34206 this.captionPackets_ = []; 34207 34208 this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define 34209 new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define 34210 new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define 34211 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 34212 ]; 34213 34214 this.reset(); 34215 34216 // forward data and done events from CCs to this CaptionStream 34217 this.ccStreams_.forEach(function (cc) { 34218 cc.on('data', this.trigger.bind(this, 'data')); 34219 cc.on('done', this.trigger.bind(this, 'done')); 34220 }, this); 34221 }; 34222 34223 CaptionStream.prototype = new stream(); 34224 CaptionStream.prototype.push = function (event) { 34225 var sei, userData; 34226 34227 // only examine SEI NALs 34228 if (event.nalUnitType !== 'sei_rbsp') { 34229 return; 34230 } 34231 34232 // parse the sei 34233 sei = parseSei(event.escapedRBSP); 34234 34235 // ignore everything but user_data_registered_itu_t_t35 34236 if (sei.payloadType !== USER_DATA_REGISTERED_ITU_T_T35) { 34237 return; 34238 } 34239 34240 // parse out the user data payload 34241 userData = parseUserData(sei); 34242 34243 // ignore unrecognized userData 34244 if (!userData) { 34245 return; 34246 } 34247 34248 // Sometimes, the same segment # will be downloaded twice. To stop the 34249 // caption data from being processed twice, we track the latest dts we've 34250 // received and ignore everything with a dts before that. However, since 34251 // data for a specific dts can be split across packets on either side of 34252 // a segment boundary, we need to make sure we *don't* ignore the packets 34253 // from the *next* segment that have dts === this.latestDts_. By constantly 34254 // tracking the number of packets received with dts === this.latestDts_, we 34255 // know how many should be ignored once we start receiving duplicates. 34256 if (event.dts < this.latestDts_) { 34257 // We've started getting older data, so set the flag. 34258 this.ignoreNextEqualDts_ = true; 34259 return; 34260 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 34261 this.numSameDts_--; 34262 if (!this.numSameDts_) { 34263 // We've received the last duplicate packet, time to start processing again 34264 this.ignoreNextEqualDts_ = false; 34265 } 34266 return; 34267 } 34268 34269 // parse out CC data packets and save them for later 34270 this.captionPackets_ = this.captionPackets_.concat(parseCaptionPackets(event.pts, userData)); 34271 if (this.latestDts_ !== event.dts) { 34272 this.numSameDts_ = 0; 34273 } 34274 this.numSameDts_++; 34275 this.latestDts_ = event.dts; 34276 }; 34277 34278 CaptionStream.prototype.flush = function () { 34279 // make sure we actually parsed captions before proceeding 34280 if (!this.captionPackets_.length) { 34281 this.ccStreams_.forEach(function (cc) { 34282 cc.flush(); 34283 }, this); 34284 return; 34285 } 34286 34287 // In Chrome, the Array#sort function is not stable so add a 34288 // presortIndex that we can use to ensure we get a stable-sort 34289 this.captionPackets_.forEach(function (elem, idx) { 34290 elem.presortIndex = idx; 34291 }); 34292 34293 // sort caption byte-pairs based on their PTS values 34294 this.captionPackets_.sort(function (a, b) { 34295 if (a.pts === b.pts) { 34296 return a.presortIndex - b.presortIndex; 34297 } 34298 return a.pts - b.pts; 34299 }); 34300 34301 this.captionPackets_.forEach(function (packet) { 34302 if (packet.type < 2) { 34303 // Dispatch packet to the right Cea608Stream 34304 this.dispatchCea608Packet(packet); 34305 } 34306 // this is where an 'else' would go for a dispatching packets 34307 // to a theoretical Cea708Stream that handles SERVICEn data 34308 }, this); 34309 34310 this.captionPackets_.length = 0; 34311 this.ccStreams_.forEach(function (cc) { 34312 cc.flush(); 34313 }, this); 34314 return; 34315 }; 34316 34317 CaptionStream.prototype.reset = function () { 34318 this.latestDts_ = null; 34319 this.ignoreNextEqualDts_ = false; 34320 this.numSameDts_ = 0; 34321 this.activeCea608Channel_ = [null, null]; 34322 this.ccStreams_.forEach(function (ccStream) { 34323 ccStream.reset(); 34324 }); 34325 }; 34326 34327 CaptionStream.prototype.dispatchCea608Packet = function (packet) { 34328 // NOTE: packet.type is the CEA608 field 34329 if (this.setsChannel1Active(packet)) { 34330 this.activeCea608Channel_[packet.type] = 0; 34331 } else if (this.setsChannel2Active(packet)) { 34332 this.activeCea608Channel_[packet.type] = 1; 34333 } 34334 if (this.activeCea608Channel_[packet.type] === null) { 34335 // If we haven't received anything to set the active channel, discard the 34336 // data; we don't want jumbled captions 34337 return; 34338 } 34339 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 34340 }; 34341 34342 CaptionStream.prototype.setsChannel1Active = function (packet) { 34343 return (packet.ccData & 0x7800) === 0x1000; 34344 }; 34345 CaptionStream.prototype.setsChannel2Active = function (packet) { 34346 return (packet.ccData & 0x7800) === 0x1800; 34347 }; 34348 34349 // ---------------------- 34350 // Session to Application 34351 // ---------------------- 34352 34353 var CHARACTER_TRANSLATION = { 34354 0x2a: 0xe1, // á 34355 0x5c: 0xe9, // é 34356 0x5e: 0xed, // à 34357 0x5f: 0xf3, // ó 34358 0x60: 0xfa, // ú 34359 0x7b: 0xe7, // ç 34360 0x7c: 0xf7, // ÷ 34361 0x7d: 0xd1, // à 34362 0x7e: 0xf1, // ñ 34363 0x7f: 0x2588, // â 34364 0x0130: 0xae, // ® 34365 0x0131: 0xb0, // ° 34366 0x0132: 0xbd, // ½ 34367 0x0133: 0xbf, // ¿ 34368 0x0134: 0x2122, // ⢠34369 0x0135: 0xa2, // ¢ 34370 0x0136: 0xa3, // £ 34371 0x0137: 0x266a, // ⪠34372 0x0138: 0xe0, // à 34373 0x0139: 0xa0, // 34374 0x013a: 0xe8, // è 34375 0x013b: 0xe2, // â 34376 0x013c: 0xea, // ê 34377 0x013d: 0xee, // î 34378 0x013e: 0xf4, // ô 34379 0x013f: 0xfb, // û 34380 0x0220: 0xc1, // à 34381 0x0221: 0xc9, // à 34382 0x0222: 0xd3, // à 34383 0x0223: 0xda, // à 34384 0x0224: 0xdc, // à 34385 0x0225: 0xfc, // ü 34386 0x0226: 0x2018, // â 34387 0x0227: 0xa1, // ¡ 34388 0x0228: 0x2a, // * 34389 0x0229: 0x27, // ' 34390 0x022a: 0x2014, // â 34391 0x022b: 0xa9, // © 34392 0x022c: 0x2120, // â 34393 0x022d: 0x2022, // ⢠34394 0x022e: 0x201c, // â 34395 0x022f: 0x201d, // â 34396 0x0230: 0xc0, // à 34397 0x0231: 0xc2, // à 34398 0x0232: 0xc7, // à 34399 0x0233: 0xc8, // à 34400 0x0234: 0xca, // à 34401 0x0235: 0xcb, // à 34402 0x0236: 0xeb, // ë 34403 0x0237: 0xce, // à 34404 0x0238: 0xcf, // à 34405 0x0239: 0xef, // ï 34406 0x023a: 0xd4, // à 34407 0x023b: 0xd9, // à 34408 0x023c: 0xf9, // ù 34409 0x023d: 0xdb, // à 34410 0x023e: 0xab, // « 34411 0x023f: 0xbb, // » 34412 0x0320: 0xc3, // à 34413 0x0321: 0xe3, // ã 34414 0x0322: 0xcd, // à 34415 0x0323: 0xcc, // à 34416 0x0324: 0xec, // ì 34417 0x0325: 0xd2, // à 34418 0x0326: 0xf2, // ò 34419 0x0327: 0xd5, // à 34420 0x0328: 0xf5, // õ 34421 0x0329: 0x7b, // { 34422 0x032a: 0x7d, // } 34423 0x032b: 0x5c, // \ 34424 0x032c: 0x5e, // ^ 34425 0x032d: 0x5f, // _ 34426 0x032e: 0x7c, // | 34427 0x032f: 0x7e, // ~ 34428 0x0330: 0xc4, // à 34429 0x0331: 0xe4, // ä 34430 0x0332: 0xd6, // à 34431 0x0333: 0xf6, // ö 34432 0x0334: 0xdf, // à 34433 0x0335: 0xa5, // Â¥ 34434 0x0336: 0xa4, // ¤ 34435 0x0337: 0x2502, // â 34436 0x0338: 0xc5, // à 34437 0x0339: 0xe5, // Ã¥ 34438 0x033a: 0xd8, // à 34439 0x033b: 0xf8, // ø 34440 0x033c: 0x250c, // â 34441 0x033d: 0x2510, // â 34442 0x033e: 0x2514, // â 34443 0x033f: 0x2518 // â 34444 }; 34445 34446 var getCharFromCode = function getCharFromCode(code) { 34447 if (code === null) { 34448 return ''; 34449 } 34450 code = CHARACTER_TRANSLATION[code] || code; 34451 return String.fromCharCode(code); 34452 }; 34453 34454 // the index of the last row in a CEA-608 display buffer 34455 var BOTTOM_ROW = 14; 34456 34457 // This array is used for mapping PACs -> row #, since there's no way of 34458 // getting it through bit logic. 34459 var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; 34460 34461 // CEA-608 captions are rendered onto a 34x15 matrix of character 34462 // cells. The "bottom" row is the last element in the outer array. 34463 var createDisplayBuffer = function createDisplayBuffer() { 34464 var result = [], 34465 i = BOTTOM_ROW + 1; 34466 while (i--) { 34467 result.push(''); 34468 } 34469 return result; 34470 }; 34471 34472 var Cea608Stream = function Cea608Stream(field, dataChannel) { 34473 Cea608Stream.prototype.init.call(this); 34474 34475 this.field_ = field || 0; 34476 this.dataChannel_ = dataChannel || 0; 34477 34478 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 34479 34480 this.setConstants(); 34481 this.reset(); 34482 34483 this.push = function (packet) { 34484 var data, swap, char0, char1, text; 34485 // remove the parity bits 34486 data = packet.ccData & 0x7f7f; 34487 34488 // ignore duplicate control codes; the spec demands they're sent twice 34489 if (data === this.lastControlCode_) { 34490 this.lastControlCode_ = null; 34491 return; 34492 } 34493 34494 // Store control codes 34495 if ((data & 0xf000) === 0x1000) { 34496 this.lastControlCode_ = data; 34497 } else if (data !== this.PADDING_) { 34498 this.lastControlCode_ = null; 34499 } 34500 34501 char0 = data >>> 8; 34502 char1 = data & 0xff;
vendor: 4,605 bytes, lines 34503-34604
34503 34504 if (data === this.PADDING_) { 34505 return; 34506 } else if (data === this.RESUME_CAPTION_LOADING_) { 34507 this.mode_ = 'popOn'; 34508 } else if (data === this.END_OF_CAPTION_) { 34509 this.clearFormatting(packet.pts); 34510 // if a caption was being displayed, it's gone now 34511 this.flushDisplayed(packet.pts); 34512 34513 // flip memory 34514 swap = this.displayed_; 34515 this.displayed_ = this.nonDisplayed_; 34516 this.nonDisplayed_ = swap; 34517 34518 // start measuring the time to display the caption 34519 this.startPts_ = packet.pts; 34520 } else if (data === this.ROLL_UP_2_ROWS_) { 34521 this.topRow_ = BOTTOM_ROW - 1; 34522 this.mode_ = 'rollUp'; 34523 } else if (data === this.ROLL_UP_3_ROWS_) { 34524 this.topRow_ = BOTTOM_ROW - 2; 34525 this.mode_ = 'rollUp'; 34526 } else if (data === this.ROLL_UP_4_ROWS_) { 34527 this.topRow_ = BOTTOM_ROW - 3; 34528 this.mode_ = 'rollUp'; 34529 } else if (data === this.CARRIAGE_RETURN_) { 34530 this.clearFormatting(packet.pts); 34531 this.flushDisplayed(packet.pts); 34532 this.shiftRowsUp_(); 34533 this.startPts_ = packet.pts; 34534 } else if (data === this.BACKSPACE_) { 34535 if (this.mode_ === 'popOn') { 34536 this.nonDisplayed_[BOTTOM_ROW] = this.nonDisplayed_[BOTTOM_ROW].slice(0, -1); 34537 } else { 34538 this.displayed_[BOTTOM_ROW] = this.displayed_[BOTTOM_ROW].slice(0, -1); 34539 } 34540 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 34541 this.flushDisplayed(packet.pts); 34542 this.displayed_ = createDisplayBuffer(); 34543 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 34544 this.nonDisplayed_ = createDisplayBuffer(); 34545 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 34546 this.mode_ = 'paintOn'; 34547 34548 // Append special characters to caption text 34549 } else if (this.isSpecialCharacter(char0, char1)) { 34550 // Bitmask char0 so that we can apply character transformations 34551 // regardless of field and data channel. 34552 // Then byte-shift to the left and OR with char1 so we can pass the 34553 // entire character code to `getCharFromCode`. 34554 char0 = (char0 & 0x03) << 8; 34555 text = getCharFromCode(char0 | char1); 34556 this[this.mode_](packet.pts, text); 34557 this.column_++; 34558 34559 // Append extended characters to caption text 34560 } else if (this.isExtCharacter(char0, char1)) { 34561 // Extended characters always follow their "non-extended" equivalents. 34562 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 34563 // decoders are supposed to drop the "è", while compliant decoders 34564 // backspace the "e" and insert "è". 34565 34566 // Delete the previous character 34567 if (this.mode_ === 'popOn') { 34568 this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); 34569 } else { 34570 this.displayed_[BOTTOM_ROW] = this.displayed_[BOTTOM_ROW].slice(0, -1); 34571 } 34572 34573 // Bitmask char0 so that we can apply character transformations 34574 // regardless of field and data channel. 34575 // Then byte-shift to the left and OR with char1 so we can pass the 34576 // entire character code to `getCharFromCode`. 34577 char0 = (char0 & 0x03) << 8; 34578 text = getCharFromCode(char0 | char1); 34579 this[this.mode_](packet.pts, text); 34580 this.column_++; 34581 34582 // Process mid-row codes 34583 } else if (this.isMidRowCode(char0, char1)) { 34584 // Attributes are not additive, so clear all formatting 34585 this.clearFormatting(packet.pts); 34586 34587 // According to the standard, mid-row codes 34588 // should be replaced with spaces, so add one now 34589 this[this.mode_](packet.pts, ' '); 34590 this.column_++; 34591 34592 if ((char1 & 0xe) === 0xe) { 34593 this.addFormatting(packet.pts, ['i']); 34594 } 34595 34596 if ((char1 & 0x1) === 0x1) { 34597 this.addFormatting(packet.pts, ['u']); 34598 } 34599 34600 // Detect offset control codes and adjust cursor 34601 } else if (this.isOffsetControlCode(char0, char1)) { 34602 // Cursor position is set by indent PAC (see below) in 4-column 34603 // increments, with an additional offset code of 1-3 to reach any 34604 // of the 32 columns specified by CEA-608. So all we need to do
vendor: 97,669 bytes, lines 34605-37275
34605 // here is increment the column cursor by the given offset. 34606 this.column_ += char1 & 0x03; 34607 34608 // Detect PACs (Preamble Address Codes) 34609 } else if (this.isPAC(char0, char1)) { 34610 34611 // There's no logic for PAC -> row mapping, so we have to just 34612 // find the row code in an array and use its index :( 34613 var row = ROWS.indexOf(data & 0x1f20); 34614 34615 if (row !== this.row_) { 34616 // formatting is only persistent for current row 34617 this.clearFormatting(packet.pts); 34618 this.row_ = row; 34619 } 34620 // All PACs can apply underline, so detect and apply 34621 // (All odd-numbered second bytes set underline) 34622 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 34623 this.addFormatting(packet.pts, ['u']); 34624 } 34625 34626 if ((data & 0x10) === 0x10) { 34627 // We've got an indent level code. Each successive even number 34628 // increments the column cursor by 4, so we can get the desired 34629 // column position by bit-shifting to the right (to get n/2) 34630 // and multiplying by 4. 34631 this.column_ = ((data & 0xe) >> 1) * 4; 34632 } 34633 34634 if (this.isColorPAC(char1)) { 34635 // it's a color code, though we only support white, which 34636 // can be either normal or italicized. white italics can be 34637 // either 0x4e or 0x6e depending on the row, so we just 34638 // bitwise-and with 0xe to see if italics should be turned on 34639 if ((char1 & 0xe) === 0xe) { 34640 this.addFormatting(packet.pts, ['i']); 34641 } 34642 } 34643 34644 // We have a normal character in char0, and possibly one in char1 34645 } else if (this.isNormalChar(char0)) { 34646 if (char1 === 0x00) { 34647 char1 = null; 34648 } 34649 text = getCharFromCode(char0); 34650 text += getCharFromCode(char1); 34651 this[this.mode_](packet.pts, text); 34652 this.column_ += text.length; 34653 } // finish data processing 34654 }; 34655 }; 34656 Cea608Stream.prototype = new stream(); 34657 // Trigger a cue point that captures the current state of the 34658 // display buffer 34659 Cea608Stream.prototype.flushDisplayed = function (pts) { 34660 var content = this.displayed_ 34661 // remove spaces from the start and end of the string 34662 .map(function (row) { 34663 return row.trim(); 34664 }) 34665 // combine all text rows to display in one cue 34666 .join('\n') 34667 // and remove blank rows from the start and end, but not the middle 34668 .replace(/^\n+|\n+$/g, ''); 34669 34670 if (content.length) { 34671 this.trigger('data', { 34672 startPts: this.startPts_, 34673 endPts: pts, 34674 text: content, 34675 stream: this.name_ 34676 }); 34677 } 34678 }; 34679 34680 /** 34681 * Zero out the data, used for startup and on seek 34682 */ 34683 Cea608Stream.prototype.reset = function () { 34684 this.mode_ = 'popOn'; 34685 // When in roll-up mode, the index of the last row that will 34686 // actually display captions. If a caption is shifted to a row 34687 // with a lower index than this, it is cleared from the display 34688 // buffer 34689 this.topRow_ = 0; 34690 this.startPts_ = 0; 34691 this.displayed_ = createDisplayBuffer(); 34692 this.nonDisplayed_ = createDisplayBuffer(); 34693 this.lastControlCode_ = null; 34694 34695 // Track row and column for proper line-breaking and spacing 34696 this.column_ = 0; 34697 this.row_ = BOTTOM_ROW; 34698 34699 // This variable holds currently-applied formatting 34700 this.formatting_ = []; 34701 }; 34702 34703 /** 34704 * Sets up control code and related constants for this instance 34705 */ 34706 Cea608Stream.prototype.setConstants = function () { 34707 // The following attributes have these uses: 34708 // ext_ : char0 for mid-row codes, and the base for extended 34709 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 34710 // extended codes) 34711 // control_: char0 for control codes, except byte-shifted to the 34712 // left so that we can do this.control_ | CONTROL_CODE 34713 // offset_: char0 for tab offset codes 34714 // 34715 // It's also worth noting that control codes, and _only_ control codes, 34716 // differ between field 1 and field2. Field 2 control codes are always 34717 // their field 1 value plus 1. That's why there's the "| field" on the 34718 // control value. 34719 if (this.dataChannel_ === 0) { 34720 this.BASE_ = 0x10; 34721 this.EXT_ = 0x11; 34722 this.CONTROL_ = (0x14 | this.field_) << 8; 34723 this.OFFSET_ = 0x17; 34724 } else if (this.dataChannel_ === 1) { 34725 this.BASE_ = 0x18; 34726 this.EXT_ = 0x19; 34727 this.CONTROL_ = (0x1c | this.field_) << 8; 34728 this.OFFSET_ = 0x1f; 34729 } 34730 34731 // Constants for the LSByte command codes recognized by Cea608Stream. This 34732 // list is not exhaustive. For a more comprehensive listing and semantics see 34733 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 34734 // Padding 34735 this.PADDING_ = 0x0000; 34736 // Pop-on Mode 34737 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 34738 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; 34739 // Roll-up Mode 34740 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 34741 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 34742 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 34743 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; 34744 // paint-on mode (not supported) 34745 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; 34746 // Erasure 34747 this.BACKSPACE_ = this.CONTROL_ | 0x21; 34748 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 34749 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 34750 }; 34751 34752 /** 34753 * Detects if the 2-byte packet data is a special character 34754 * 34755 * Special characters have a second byte in the range 0x30 to 0x3f, 34756 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 34757 * data channel 2). 34758 * 34759 * @param {Integer} char0 The first byte 34760 * @param {Integer} char1 The second byte 34761 * @return {Boolean} Whether the 2 bytes are an special character 34762 */ 34763 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 34764 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 34765 }; 34766 34767 /** 34768 * Detects if the 2-byte packet data is an extended character 34769 * 34770 * Extended characters have a second byte in the range 0x20 to 0x3f, 34771 * with the first byte being 0x12 or 0x13 (for data channel 1) or 34772 * 0x1a or 0x1b (for data channel 2). 34773 * 34774 * @param {Integer} char0 The first byte 34775 * @param {Integer} char1 The second byte 34776 * @return {Boolean} Whether the 2 bytes are an extended character 34777 */ 34778 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 34779 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 34780 }; 34781 34782 /** 34783 * Detects if the 2-byte packet is a mid-row code 34784 * 34785 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 34786 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 34787 * channel 2). 34788 * 34789 * @param {Integer} char0 The first byte 34790 * @param {Integer} char1 The second byte 34791 * @return {Boolean} Whether the 2 bytes are a mid-row code 34792 */ 34793 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 34794 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 34795 }; 34796 34797 /** 34798 * Detects if the 2-byte packet is an offset control code 34799 * 34800 * Offset control codes have a second byte in the range 0x21 to 0x23, 34801 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 34802 * data channel 2). 34803 * 34804 * @param {Integer} char0 The first byte 34805 * @param {Integer} char1 The second byte 34806 * @return {Boolean} Whether the 2 bytes are an offset control code 34807 */ 34808 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 34809 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 34810 }; 34811 34812 /** 34813 * Detects if the 2-byte packet is a Preamble Address Code 34814 * 34815 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 34816 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 34817 * range 0x40 to 0x7f. 34818 * 34819 * @param {Integer} char0 The first byte 34820 * @param {Integer} char1 The second byte 34821 * @return {Boolean} Whether the 2 bytes are a PAC 34822 */ 34823 Cea608Stream.prototype.isPAC = function (char0, char1) { 34824 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 34825 }; 34826 34827 /** 34828 * Detects if a packet's second byte is in the range of a PAC color code 34829 * 34830 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 34831 * 0x60 to 0x6f. 34832 * 34833 * @param {Integer} char1 The second byte 34834 * @return {Boolean} Whether the byte is a color PAC 34835 */ 34836 Cea608Stream.prototype.isColorPAC = function (char1) { 34837 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 34838 }; 34839 34840 /** 34841 * Detects if a single byte is in the range of a normal character 34842 * 34843 * Normal text bytes are in the range 0x20 to 0x7f. 34844 * 34845 * @param {Integer} char The byte 34846 * @return {Boolean} Whether the byte is a normal character 34847 */ 34848 Cea608Stream.prototype.isNormalChar = function (char) { 34849 return char >= 0x20 && char <= 0x7f; 34850 }; 34851 34852 // Adds the opening HTML tag for the passed character to the caption text, 34853 // and keeps track of it for later closing 34854 Cea608Stream.prototype.addFormatting = function (pts, format) { 34855 this.formatting_ = this.formatting_.concat(format); 34856 var text = format.reduce(function (text, format) { 34857 return text + '<' + format + '>'; 34858 }, ''); 34859 this[this.mode_](pts, text); 34860 }; 34861 34862 // Adds HTML closing tags for current formatting to caption text and 34863 // clears remembered formatting 34864 Cea608Stream.prototype.clearFormatting = function (pts) { 34865 if (!this.formatting_.length) { 34866 return; 34867 } 34868 var text = this.formatting_.reverse().reduce(function (text, format) { 34869 return text + '</' + format + '>'; 34870 }, ''); 34871 this.formatting_ = []; 34872 this[this.mode_](pts, text); 34873 }; 34874 34875 // Mode Implementations 34876 Cea608Stream.prototype.popOn = function (pts, text) { 34877 var baseRow = this.nonDisplayed_[this.row_]; 34878 34879 // buffer characters 34880 baseRow += text; 34881 this.nonDisplayed_[this.row_] = baseRow; 34882 }; 34883 34884 Cea608Stream.prototype.rollUp = function (pts, text) { 34885 var baseRow = this.displayed_[BOTTOM_ROW]; 34886 34887 baseRow += text; 34888 this.displayed_[BOTTOM_ROW] = baseRow; 34889 }; 34890 34891 Cea608Stream.prototype.shiftRowsUp_ = function () { 34892 var i; 34893 // clear out inactive rows 34894 for (i = 0; i < this.topRow_; i++) { 34895 this.displayed_[i] = ''; 34896 } 34897 // shift displayed rows up 34898 for (i = this.topRow_; i < BOTTOM_ROW; i++) { 34899 this.displayed_[i] = this.displayed_[i + 1]; 34900 } 34901 // clear out the bottom row 34902 this.displayed_[BOTTOM_ROW] = ''; 34903 }; 34904 34905 // paintOn mode is not implemented 34906 Cea608Stream.prototype.paintOn = function () {}; 34907 34908 // exports 34909 var captionStream = { 34910 CaptionStream: CaptionStream, 34911 Cea608Stream: Cea608Stream 34912 }; 34913 34914 var streamTypes = { 34915 H264_STREAM_TYPE: 0x1B, 34916 ADTS_STREAM_TYPE: 0x0F, 34917 METADATA_STREAM_TYPE: 0x15 34918 }; 34919 34920 var MAX_TS = 8589934592; 34921 34922 var RO_THRESH = 4294967296; 34923 34924 var handleRollover = function handleRollover(value, reference) { 34925 var direction = 1; 34926 34927 if (value > reference) { 34928 // If the current timestamp value is greater than our reference timestamp and we detect a 34929 // timestamp rollover, this means the roll over is happening in the opposite direction. 34930 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 34931 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 34932 // rollover point. In loading this segment, the timestamp values will be very large, 34933 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 34934 // the time stamp to be `value - 2^33`. 34935 direction = -1; 34936 } 34937 34938 // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 34939 // cause an incorrect adjustment. 34940 while (Math.abs(reference - value) > RO_THRESH) { 34941 value += direction * MAX_TS; 34942 } 34943 34944 return value; 34945 }; 34946 34947 var TimestampRolloverStream = function TimestampRolloverStream(type) { 34948 var lastDTS, referenceDTS; 34949 34950 TimestampRolloverStream.prototype.init.call(this); 34951 34952 this.type_ = type; 34953 34954 this.push = function (data) { 34955 if (data.type !== this.type_) { 34956 return; 34957 } 34958 34959 if (referenceDTS === undefined) { 34960 referenceDTS = data.dts; 34961 } 34962 34963 data.dts = handleRollover(data.dts, referenceDTS); 34964 data.pts = handleRollover(data.pts, referenceDTS); 34965 34966 lastDTS = data.dts; 34967 34968 this.trigger('data', data); 34969 }; 34970 34971 this.flush = function () { 34972 referenceDTS = lastDTS; 34973 this.trigger('done'); 34974 }; 34975 34976 this.discontinuity = function () { 34977 referenceDTS = void 0; 34978 lastDTS = void 0; 34979 }; 34980 }; 34981 34982 TimestampRolloverStream.prototype = new stream(); 34983 34984 var timestampRolloverStream = { 34985 TimestampRolloverStream: TimestampRolloverStream, 34986 handleRollover: handleRollover 34987 }; 34988 34989 var percentEncode = function percentEncode(bytes, start, end) { 34990 var i, 34991 result = ''; 34992 for (i = start; i < end; i++) { 34993 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 34994 } 34995 return result; 34996 }, 34997 34998 34999 // return the string representation of the specified byte range, 35000 // interpreted as UTf-8. 35001 parseUtf8 = function parseUtf8(bytes, start, end) { 35002 return decodeURIComponent(percentEncode(bytes, start, end)); 35003 }, 35004 35005 35006 // return the string representation of the specified byte range, 35007 // interpreted as ISO-8859-1. 35008 parseIso88591 = function parseIso88591(bytes, start, end) { 35009 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 35010 }, 35011 parseSyncSafeInteger = function parseSyncSafeInteger(data) { 35012 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 35013 }, 35014 tagParsers = { 35015 TXXX: function TXXX(tag) { 35016 var i; 35017 if (tag.data[0] !== 3) { 35018 // ignore frames with unrecognized character encodings 35019 return; 35020 } 35021 35022 for (i = 1; i < tag.data.length; i++) { 35023 if (tag.data[i] === 0) { 35024 // parse the text fields 35025 tag.description = parseUtf8(tag.data, 1, i); 35026 // do not include the null terminator in the tag value 35027 tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, ''); 35028 break; 35029 } 35030 } 35031 tag.data = tag.value; 35032 }, 35033 WXXX: function WXXX(tag) { 35034 var i; 35035 if (tag.data[0] !== 3) { 35036 // ignore frames with unrecognized character encodings 35037 return; 35038 } 35039 35040 for (i = 1; i < tag.data.length; i++) { 35041 if (tag.data[i] === 0) { 35042 // parse the description and URL fields 35043 tag.description = parseUtf8(tag.data, 1, i); 35044 tag.url = parseUtf8(tag.data, i + 1, tag.data.length); 35045 break; 35046 } 35047 } 35048 }, 35049 PRIV: function PRIV(tag) { 35050 var i; 35051 35052 for (i = 0; i < tag.data.length; i++) { 35053 if (tag.data[i] === 0) { 35054 // parse the description and URL fields 35055 tag.owner = parseIso88591(tag.data, 0, i); 35056 break; 35057 } 35058 } 35059 tag.privateData = tag.data.subarray(i + 1); 35060 tag.data = tag.privateData; 35061 } 35062 }, 35063 _MetadataStream; 35064 35065 _MetadataStream = function MetadataStream(options) { 35066 var settings = { 35067 debug: !!(options && options.debug), 35068 35069 // the bytes of the program-level descriptor field in MP2T 35070 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 35071 // program element descriptors" 35072 descriptor: options && options.descriptor 35073 }, 35074 35075 35076 // the total size in bytes of the ID3 tag being parsed 35077 tagSize = 0, 35078 35079 35080 // tag data that is not complete enough to be parsed 35081 buffer = [], 35082 35083 35084 // the total number of bytes currently in the buffer 35085 bufferSize = 0, 35086 i; 35087 35088 _MetadataStream.prototype.init.call(this); 35089 35090 // calculate the text track in-band metadata track dispatch type 35091 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 35092 this.dispatchType = streamTypes.METADATA_STREAM_TYPE.toString(16); 35093 if (settings.descriptor) { 35094 for (i = 0; i < settings.descriptor.length; i++) { 35095 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 35096 } 35097 } 35098 35099 this.push = function (chunk) { 35100 var tag, frameStart, frameSize, frame, i, frameHeader; 35101 if (chunk.type !== 'timed-metadata') { 35102 return; 35103 } 35104 35105 // if data_alignment_indicator is set in the PES header, 35106 // we must have the start of a new ID3 tag. Assume anything 35107 // remaining in the buffer was malformed and throw it out 35108 if (chunk.dataAlignmentIndicator) { 35109 bufferSize = 0; 35110 buffer.length = 0; 35111 } 35112 35113 // ignore events that don't look like ID3 data 35114 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))) { 35115 if (settings.debug) { 35116 // eslint-disable-next-line no-console 35117 console.log('Skipping unrecognized metadata packet'); 35118 } 35119 return; 35120 } 35121 35122 // add this chunk to the data we've collected so far 35123 35124 buffer.push(chunk); 35125 bufferSize += chunk.data.byteLength; 35126 35127 // grab the size of the entire frame from the ID3 header 35128 if (buffer.length === 1) { 35129 // the frame size is transmitted as a 28-bit integer in the 35130 // last four bytes of the ID3 header. 35131 // The most significant bit of each byte is dropped and the 35132 // results concatenated to recover the actual value. 35133 tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10)); 35134 35135 // ID3 reports the tag size excluding the header but it's more 35136 // convenient for our comparisons to include it 35137 tagSize += 10; 35138 } 35139 35140 // if the entire frame has not arrived, wait for more data 35141 if (bufferSize < tagSize) { 35142 return; 35143 } 35144 35145 // collect the entire frame so it can be parsed 35146 tag = { 35147 data: new Uint8Array(tagSize), 35148 frames: [], 35149 pts: buffer[0].pts, 35150 dts: buffer[0].dts 35151 }; 35152 for (i = 0; i < tagSize;) { 35153 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 35154 i += buffer[0].data.byteLength; 35155 bufferSize -= buffer[0].data.byteLength; 35156 buffer.shift(); 35157 } 35158 35159 // find the start of the first frame and the end of the tag 35160 frameStart = 10; 35161 if (tag.data[5] & 0x40) { 35162 // advance the frame start past the extended header 35163 frameStart += 4; // header size field 35164 frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14)); 35165 35166 // clip any padding off the end 35167 tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20)); 35168 } 35169 35170 // parse one or more ID3 frames 35171 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 35172 do { 35173 // determine the number of bytes in this frame 35174 frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 35175 if (frameSize < 1) { 35176 // eslint-disable-next-line no-console 35177 return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); 35178 } 35179 frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]); 35180 35181 frame = { 35182 id: frameHeader, 35183 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) 35184 }; 35185 frame.key = frame.id; 35186 if (tagParsers[frame.id]) { 35187 tagParsers[frame.id](frame); 35188 35189 // handle the special PRIV frame used to indicate the start 35190 // time for raw AAC data 35191 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 35192 var d = frame.data, 35193 size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 35194 35195 size *= 4; 35196 size += d[7] & 0x03; 35197 frame.timeStamp = size; 35198 // in raw AAC, all subsequent data will be timestamped based 35199 // on the value of this frame 35200 // we couldn't have known the appropriate pts and dts before 35201 // parsing this ID3 tag so set those values now 35202 if (tag.pts === undefined && tag.dts === undefined) { 35203 tag.pts = frame.timeStamp; 35204 tag.dts = frame.timeStamp; 35205 } 35206 this.trigger('timestamp', frame); 35207 } 35208 } 35209 tag.frames.push(frame); 35210 35211 frameStart += 10; // advance past the frame header 35212 frameStart += frameSize; // advance past the frame body 35213 } while (frameStart < tagSize); 35214 this.trigger('data', tag); 35215 }; 35216 }; 35217 _MetadataStream.prototype = new stream(); 35218 35219 var metadataStream = _MetadataStream; 35220 35221 var TimestampRolloverStream$1 = timestampRolloverStream.TimestampRolloverStream; 35222 35223 // object types 35224 var _TransportPacketStream, _TransportParseStream, _ElementaryStream; 35225 35226 // constants 35227 var MP2T_PACKET_LENGTH = 188, 35228 35229 35230 // bytes 35231 SYNC_BYTE = 0x47; 35232 35233 /** 35234 * Splits an incoming stream of binary data into MPEG-2 Transport 35235 * Stream packets. 35236 */ 35237 _TransportPacketStream = function TransportPacketStream() { 35238 var buffer = new Uint8Array(MP2T_PACKET_LENGTH), 35239 bytesInBuffer = 0; 35240 35241 _TransportPacketStream.prototype.init.call(this); 35242 35243 // Deliver new bytes to the stream. 35244 35245 this.push = function (bytes) { 35246 var startIndex = 0, 35247 endIndex = MP2T_PACKET_LENGTH, 35248 everything; 35249 35250 // If there are bytes remaining from the last segment, prepend them to the 35251 // bytes that were pushed in 35252 if (bytesInBuffer) { 35253 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 35254 everything.set(buffer.subarray(0, bytesInBuffer)); 35255 everything.set(bytes, bytesInBuffer); 35256 bytesInBuffer = 0; 35257 } else { 35258 everything = bytes; 35259 } 35260 35261 // While we have enough data for a packet 35262 while (endIndex < everything.byteLength) { 35263 // Look for a pair of start and end sync bytes in the data.. 35264 if (everything[startIndex] === SYNC_BYTE && everything[endIndex] === SYNC_BYTE) { 35265 // We found a packet so emit it and jump one whole packet forward in 35266 // the stream 35267 this.trigger('data', everything.subarray(startIndex, endIndex)); 35268 startIndex += MP2T_PACKET_LENGTH; 35269 endIndex += MP2T_PACKET_LENGTH; 35270 continue; 35271 } 35272 // If we get here, we have somehow become de-synchronized and we need to step 35273 // forward one byte at a time until we find a pair of sync bytes that denote 35274 // a packet 35275 startIndex++; 35276 endIndex++; 35277 } 35278 35279 // If there was some data left over at the end of the segment that couldn't 35280 // possibly be a whole packet, keep it because it might be the start of a packet 35281 // that continues in the next segment 35282 if (startIndex < everything.byteLength) { 35283 buffer.set(everything.subarray(startIndex), 0); 35284 bytesInBuffer = everything.byteLength - startIndex; 35285 } 35286 }; 35287 35288 this.flush = function () { 35289 // If the buffer contains a whole packet when we are being flushed, emit it 35290 // and empty the buffer. Otherwise hold onto the data because it may be 35291 // important for decoding the next segment 35292 if (bytesInBuffer === MP2T_PACKET_LENGTH && buffer[0] === SYNC_BYTE) { 35293 this.trigger('data', buffer); 35294 bytesInBuffer = 0; 35295 } 35296 this.trigger('done'); 35297 }; 35298 }; 35299 _TransportPacketStream.prototype = new stream(); 35300 35301 /** 35302 * Accepts an MP2T TransportPacketStream and emits data events with parsed 35303 * forms of the individual transport stream packets. 35304 */ 35305 _TransportParseStream = function TransportParseStream() { 35306 var parsePsi, parsePat, parsePmt, self; 35307 _TransportParseStream.prototype.init.call(this); 35308 self = this; 35309 35310 this.packetsWaitingForPmt = []; 35311 this.programMapTable = undefined; 35312 35313 parsePsi = function parsePsi(payload, psi) { 35314 var offset = 0; 35315 35316 // PSI packets may be split into multiple sections and those 35317 // sections may be split into multiple packets. If a PSI 35318 // section starts in this packet, the payload_unit_start_indicator 35319 // will be true and the first byte of the payload will indicate 35320 // the offset from the current position to the start of the 35321 // section. 35322 if (psi.payloadUnitStartIndicator) { 35323 offset += payload[offset] + 1; 35324 } 35325 35326 if (psi.type === 'pat') { 35327 parsePat(payload.subarray(offset), psi); 35328 } else { 35329 parsePmt(payload.subarray(offset), psi); 35330 } 35331 }; 35332 35333 parsePat = function parsePat(payload, pat) { 35334 pat.section_number = payload[7]; // eslint-disable-line camelcase 35335 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 35336 35337 // skip the PSI header and parse the first PMT entry 35338 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; 35339 pat.pmtPid = self.pmtPid; 35340 }; 35341 35342 /** 35343 * Parse out the relevant fields of a Program Map Table (PMT). 35344 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 35345 * packet. The first byte in this array should be the table_id 35346 * field. 35347 * @param pmt {object} the object that should be decorated with 35348 * fields parsed from the PMT. 35349 */ 35350 parsePmt = function parsePmt(payload, pmt) { 35351 var sectionLength, tableEnd, programInfoLength, offset; 35352 35353 // PMTs can be sent ahead of the time when they should actually 35354 // take effect. We don't believe this should ever be the case 35355 // for HLS but we'll ignore "forward" PMT declarations if we see 35356 // them. Future PMT declarations have the current_next_indicator 35357 // set to zero. 35358 if (!(payload[5] & 0x01)) { 35359 return; 35360 } 35361 35362 // overwrite any existing program map table 35363 self.programMapTable = { 35364 video: null, 35365 audio: null, 35366 'timed-metadata': {} 35367 }; 35368 35369 // the mapping table ends at the end of the current section 35370 sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; 35371 tableEnd = 3 + sectionLength - 4; 35372 35373 // to determine where the table is, we have to figure out how 35374 // long the program info descriptors are 35375 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; 35376 35377 // advance the offset to the first entry in the mapping table 35378 offset = 12 + programInfoLength; 35379 while (offset < tableEnd) { 35380 var streamType = payload[offset]; 35381 var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; 35382 35383 // only map a single elementary_pid for audio and video stream types 35384 // TODO: should this be done for metadata too? for now maintain behavior of 35385 // multiple metadata streams 35386 if (streamType === streamTypes.H264_STREAM_TYPE && self.programMapTable.video === null) { 35387 self.programMapTable.video = pid; 35388 } else if (streamType === streamTypes.ADTS_STREAM_TYPE && self.programMapTable.audio === null) { 35389 self.programMapTable.audio = pid; 35390 } else if (streamType === streamTypes.METADATA_STREAM_TYPE) { 35391 // map pid to stream type for metadata streams 35392 self.programMapTable['timed-metadata'][pid] = streamType; 35393 } 35394 35395 // move to the next table entry 35396 // skip past the elementary stream descriptors, if present 35397 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; 35398 } 35399 35400 // record the map on the packet as well 35401 pmt.programMapTable = self.programMapTable; 35402 }; 35403 35404 /** 35405 * Deliver a new MP2T packet to the stream. 35406 */ 35407 this.push = function (packet) { 35408 var result = {}, 35409 offset = 4; 35410 35411 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); 35412 35413 // pid is a 13-bit field starting at the last bit of packet[1] 35414 result.pid = packet[1] & 0x1f; 35415 result.pid <<= 8; 35416 result.pid |= packet[2]; 35417 35418 // if an adaption field is present, its length is specified by the 35419 // fifth byte of the TS packet header. The adaptation field is 35420 // used to add stuffing to PES packets that don't fill a complete 35421 // TS packet, and to specify some forms of timing and control data 35422 // that we do not currently use. 35423 if ((packet[3] & 0x30) >>> 4 > 0x01) { 35424 offset += packet[offset] + 1; 35425 } 35426 35427 // parse the rest of the packet based on the type 35428 if (result.pid === 0) { 35429 result.type = 'pat'; 35430 parsePsi(packet.subarray(offset), result); 35431 this.trigger('data', result); 35432 } else if (result.pid === this.pmtPid) { 35433 result.type = 'pmt'; 35434 parsePsi(packet.subarray(offset), result); 35435 this.trigger('data', result); 35436 35437 // if there are any packets waiting for a PMT to be found, process them now 35438 while (this.packetsWaitingForPmt.length) { 35439 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 35440 } 35441 } else if (this.programMapTable === undefined) { 35442 // When we have not seen a PMT yet, defer further processing of 35443 // PES packets until one has been parsed 35444 this.packetsWaitingForPmt.push([packet, offset, result]); 35445 } else { 35446 this.processPes_(packet, offset, result); 35447 } 35448 }; 35449 35450 this.processPes_ = function (packet, offset, result) { 35451 // set the appropriate stream type 35452 if (result.pid === this.programMapTable.video) { 35453 result.streamType = streamTypes.H264_STREAM_TYPE; 35454 } else if (result.pid === this.programMapTable.audio) { 35455 result.streamType = streamTypes.ADTS_STREAM_TYPE; 35456 } else { 35457 // if not video or audio, it is timed-metadata or unknown 35458 // if unknown, streamType will be undefined 35459 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 35460 } 35461 35462 result.type = 'pes'; 35463 result.data = packet.subarray(offset); 35464 35465 this.trigger('data', result); 35466 }; 35467 }; 35468 _TransportParseStream.prototype = new stream(); 35469 _TransportParseStream.STREAM_TYPES = { 35470 h264: 0x1b, 35471 adts: 0x0f 35472 }; 35473 35474 /** 35475 * Reconsistutes program elementary stream (PES) packets from parsed 35476 * transport stream packets. That is, if you pipe an 35477 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 35478 * events will be events which capture the bytes for individual PES 35479 * packets plus relevant metadata that has been extracted from the 35480 * container. 35481 */ 35482 _ElementaryStream = function ElementaryStream() { 35483 var self = this, 35484 35485 35486 // PES packet fragments 35487 video = { 35488 data: [], 35489 size: 0 35490 }, 35491 audio = { 35492 data: [], 35493 size: 0 35494 }, 35495 timedMetadata = { 35496 data: [], 35497 size: 0 35498 }, 35499 parsePes = function parsePes(payload, pes) { 35500 var ptsDtsFlags; 35501 35502 // get the packet length, this will be 0 for video 35503 pes.packetLength = 6 + (payload[4] << 8 | payload[5]); 35504 35505 // find out if this packets starts a new keyframe 35506 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; 35507 // PES packets may be annotated with a PTS value, or a PTS value 35508 // and a DTS value. Determine what combination of values is 35509 // available to work with. 35510 ptsDtsFlags = payload[7]; 35511 35512 // PTS and DTS are normally stored as a 33-bit number. Javascript 35513 // performs all bitwise operations on 32-bit integers but javascript 35514 // supports a much greater range (52-bits) of integer using standard 35515 // mathematical operations. 35516 // We construct a 31-bit value using bitwise operators over the 31 35517 // most significant bits and then multiply by 4 (equal to a left-shift 35518 // of 2) before we add the final 2 least significant bits of the 35519 // timestamp (equal to an OR.) 35520 if (ptsDtsFlags & 0xC0) { 35521 // the PTS and DTS are not written out directly. For information 35522 // on how they are encoded, see 35523 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 35524 pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3; 35525 pes.pts *= 4; // Left shift by 2 35526 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 35527 pes.dts = pes.pts; 35528 if (ptsDtsFlags & 0x40) { 35529 pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3; 35530 pes.dts *= 4; // Left shift by 2 35531 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 35532 } 35533 } 35534 // the data section starts immediately after the PES header. 35535 // pes_header_data_length specifies the number of header bytes 35536 // that follow the last byte of the field. 35537 pes.data = payload.subarray(9 + payload[8]); 35538 }, 35539 flushStream = function flushStream(stream$$1, type, forceFlush) { 35540 var packetData = new Uint8Array(stream$$1.size), 35541 event = { 35542 type: type 35543 }, 35544 i = 0, 35545 offset = 0, 35546 packetFlushable = false, 35547 fragment; 35548 35549 // do nothing if there is not enough buffered data for a complete 35550 // PES header 35551 if (!stream$$1.data.length || stream$$1.size < 9) { 35552 return; 35553 } 35554 event.trackId = stream$$1.data[0].pid; 35555 35556 // reassemble the packet 35557 for (i = 0; i < stream$$1.data.length; i++) { 35558 fragment = stream$$1.data[i]; 35559 35560 packetData.set(fragment.data, offset); 35561 offset += fragment.data.byteLength; 35562 } 35563 35564 // parse assembled packet's PES header 35565 parsePes(packetData, event); 35566 35567 // non-video PES packets MUST have a non-zero PES_packet_length 35568 // check that there is enough stream data to fill the packet 35569 packetFlushable = type === 'video' || event.packetLength <= stream$$1.size; 35570 35571 // flush pending packets if the conditions are right 35572 if (forceFlush || packetFlushable) { 35573 stream$$1.size = 0; 35574 stream$$1.data.length = 0; 35575 } 35576 35577 // only emit packets that are complete. this is to avoid assembling 35578 // incomplete PES packets due to poor segmentation 35579 if (packetFlushable) { 35580 self.trigger('data', event); 35581 } 35582 }; 35583 35584 _ElementaryStream.prototype.init.call(this); 35585 35586 this.push = function (data) { 35587 ({ 35588 pat: function pat() { 35589 // we have to wait for the PMT to arrive as well before we 35590 // have any meaningful metadata 35591 }, 35592 pes: function pes() { 35593 var stream$$1, streamType; 35594 35595 switch (data.streamType) { 35596 case streamTypes.H264_STREAM_TYPE: 35597 case streamTypes.H264_STREAM_TYPE: 35598 stream$$1 = video; 35599 streamType = 'video'; 35600 break; 35601 case streamTypes.ADTS_STREAM_TYPE: 35602 stream$$1 = audio; 35603 streamType = 'audio'; 35604 break; 35605 case streamTypes.METADATA_STREAM_TYPE: 35606 stream$$1 = timedMetadata; 35607 streamType = 'timed-metadata'; 35608 break; 35609 default: 35610 // ignore unknown stream types 35611 return; 35612 } 35613 35614 // if a new packet is starting, we can flush the completed 35615 // packet 35616 if (data.payloadUnitStartIndicator) { 35617 flushStream(stream$$1, streamType, true); 35618 } 35619 35620 // buffer this fragment until we are sure we've received the 35621 // complete payload 35622 stream$$1.data.push(data); 35623 stream$$1.size += data.data.byteLength; 35624 }, 35625 pmt: function pmt() { 35626 var event = { 35627 type: 'metadata', 35628 tracks: [] 35629 }, 35630 programMapTable = data.programMapTable; 35631 35632 // translate audio and video streams to tracks 35633 if (programMapTable.video !== null) { 35634 event.tracks.push({ 35635 timelineStartInfo: { 35636 baseMediaDecodeTime: 0 35637 }, 35638 id: +programMapTable.video, 35639 codec: 'avc', 35640 type: 'video' 35641 }); 35642 } 35643 if (programMapTable.audio !== null) { 35644 event.tracks.push({ 35645 timelineStartInfo: { 35646 baseMediaDecodeTime: 0 35647 }, 35648 id: +programMapTable.audio, 35649 codec: 'adts', 35650 type: 'audio' 35651 }); 35652 } 35653 35654 self.trigger('data', event); 35655 } 35656 })[data.type](); 35657 }; 35658 35659 /** 35660 * Flush any remaining input. Video PES packets may be of variable 35661 * length. Normally, the start of a new video packet can trigger the 35662 * finalization of the previous packet. That is not possible if no 35663 * more video is forthcoming, however. In that case, some other 35664 * mechanism (like the end of the file) has to be employed. When it is 35665 * clear that no additional data is forthcoming, calling this method 35666 * will flush the buffered packets. 35667 */ 35668 this.flush = function () { 35669 // !!THIS ORDER IS IMPORTANT!! 35670 // video first then audio 35671 flushStream(video, 'video'); 35672 flushStream(audio, 'audio'); 35673 flushStream(timedMetadata, 'timed-metadata'); 35674 this.trigger('done'); 35675 }; 35676 }; 35677 _ElementaryStream.prototype = new stream(); 35678 35679 var m2ts = { 35680 PAT_PID: 0x0000, 35681 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH, 35682 TransportPacketStream: _TransportPacketStream, 35683 TransportParseStream: _TransportParseStream, 35684 ElementaryStream: _ElementaryStream, 35685 TimestampRolloverStream: TimestampRolloverStream$1, 35686 CaptionStream: captionStream.CaptionStream, 35687 Cea608Stream: captionStream.Cea608Stream, 35688 MetadataStream: metadataStream 35689 }; 35690 35691 for (var type in streamTypes) { 35692 if (streamTypes.hasOwnProperty(type)) { 35693 m2ts[type] = streamTypes[type]; 35694 } 35695 } 35696 35697 var m2ts_1 = m2ts; 35698 35699 var _AdtsStream; 35700 35701 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 35702 35703 /* 35704 * Accepts a ElementaryStream and emits data events with parsed 35705 * AAC Audio Frames of the individual packets. Input audio in ADTS 35706 * format is unpacked and re-emitted as AAC frames. 35707 * 35708 * @see http://wiki.multimedia.cx/index.php?title=ADTS 35709 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 35710 */ 35711 _AdtsStream = function AdtsStream() { 35712 var buffer; 35713 35714 _AdtsStream.prototype.init.call(this); 35715 35716 this.push = function (packet) { 35717 var i = 0, 35718 frameNum = 0, 35719 frameLength, 35720 protectionSkipBytes, 35721 frameEnd, 35722 oldBuffer, 35723 sampleCount, 35724 adtsFrameDuration; 35725 35726 if (packet.type !== 'audio') { 35727 // ignore non-audio data 35728 return; 35729 } 35730 35731 // Prepend any data in the buffer to the input data so that we can parse 35732 // aac frames the cross a PES packet boundary 35733 if (buffer) { 35734 oldBuffer = buffer; 35735 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 35736 buffer.set(oldBuffer); 35737 buffer.set(packet.data, oldBuffer.byteLength); 35738 } else { 35739 buffer = packet.data; 35740 } 35741 35742 // unpack any ADTS frames which have been fully received 35743 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 35744 while (i + 5 < buffer.length) { 35745 35746 // Loook for the start of an ADTS header.. 35747 if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) { 35748 // If a valid header was not found, jump one forward and attempt to 35749 // find a valid ADTS header starting at the next byte 35750 i++; 35751 continue; 35752 } 35753 35754 // The protection skip bit tells us if we have 2 bytes of CRC data at the 35755 // end of the ADTS header 35756 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; 35757 35758 // Frame length is a 13 bit integer starting 16 bits from the 35759 // end of the sync sequence 35760 frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5; 35761 35762 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 35763 adtsFrameDuration = sampleCount * 90000 / ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2]; 35764 35765 frameEnd = i + frameLength; 35766 35767 // If we don't have enough data to actually finish this ADTS frame, return 35768 // and wait for more data 35769 if (buffer.byteLength < frameEnd) { 35770 return; 35771 } 35772 35773 // Otherwise, deliver the complete AAC frame 35774 this.trigger('data', { 35775 pts: packet.pts + frameNum * adtsFrameDuration, 35776 dts: packet.dts + frameNum * adtsFrameDuration, 35777 sampleCount: sampleCount, 35778 audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1, 35779 channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6, 35780 samplerate: ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2], 35781 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 35782 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 35783 samplesize: 16, 35784 data: buffer.subarray(i + 7 + protectionSkipBytes, frameEnd) 35785 }); 35786 35787 // If the buffer is empty, clear it and return 35788 if (buffer.byteLength === frameEnd) { 35789 buffer = undefined; 35790 return; 35791 } 35792 35793 frameNum++; 35794 35795 // Remove the finished frame from the buffer and start the process again 35796 buffer = buffer.subarray(frameEnd); 35797 } 35798 }; 35799 this.flush = function () { 35800 this.trigger('done'); 35801 }; 35802 }; 35803 35804 _AdtsStream.prototype = new stream(); 35805 35806 var adts = _AdtsStream; 35807 35808 var ExpGolomb; 35809 35810 /** 35811 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 35812 * scheme used by h264. 35813 */ 35814 ExpGolomb = function ExpGolomb(workingData) { 35815 var 35816 // the number of bytes left to examine in workingData 35817 workingBytesAvailable = workingData.byteLength, 35818 35819 35820 // the current word being examined 35821 workingWord = 0, 35822 35823 35824 // :uint 35825 35826 // the number of bits left to examine in the current word 35827 workingBitsAvailable = 0; // :uint; 35828 35829 // ():uint 35830 this.length = function () { 35831 return 8 * workingBytesAvailable; 35832 }; 35833 35834 // ():uint 35835 this.bitsAvailable = function () { 35836 return 8 * workingBytesAvailable + workingBitsAvailable; 35837 }; 35838 35839 // ():void 35840 this.loadWord = function () { 35841 var position = workingData.byteLength - workingBytesAvailable, 35842 workingBytes = new Uint8Array(4), 35843 availableBytes = Math.min(4, workingBytesAvailable); 35844 35845 if (availableBytes === 0) { 35846 throw new Error('no bytes available'); 35847 } 35848 35849 workingBytes.set(workingData.subarray(position, position + availableBytes)); 35850 workingWord = new DataView(workingBytes.buffer).getUint32(0); 35851 35852 // track the amount of workingData that has been processed 35853 workingBitsAvailable = availableBytes * 8; 35854 workingBytesAvailable -= availableBytes; 35855 }; 35856 35857 // (count:int):void 35858 this.skipBits = function (count) { 35859 var skipBytes; // :int 35860 if (workingBitsAvailable > count) { 35861 workingWord <<= count; 35862 workingBitsAvailable -= count; 35863 } else { 35864 count -= workingBitsAvailable; 35865 skipBytes = Math.floor(count / 8); 35866 35867 count -= skipBytes * 8; 35868 workingBytesAvailable -= skipBytes; 35869 35870 this.loadWord(); 35871 35872 workingWord <<= count; 35873 workingBitsAvailable -= count; 35874 } 35875 }; 35876 35877 // (size:int):uint 35878 this.readBits = function (size) { 35879 var bits = Math.min(workingBitsAvailable, size), 35880 35881 35882 // :uint 35883 valu = workingWord >>> 32 - bits; // :uint 35884 // if size > 31, handle error 35885 workingBitsAvailable -= bits; 35886 if (workingBitsAvailable > 0) { 35887 workingWord <<= bits; 35888 } else if (workingBytesAvailable > 0) { 35889 this.loadWord(); 35890 } 35891 35892 bits = size - bits; 35893 if (bits > 0) { 35894 return valu << bits | this.readBits(bits); 35895 } 35896 return valu; 35897 }; 35898 35899 // ():uint 35900 this.skipLeadingZeros = function () { 35901 var leadingZeroCount; // :uint 35902 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) { 35903 if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) { 35904 // the first bit of working word is 1 35905 workingWord <<= leadingZeroCount; 35906 workingBitsAvailable -= leadingZeroCount; 35907 return leadingZeroCount; 35908 } 35909 } 35910 35911 // we exhausted workingWord and still have not found a 1 35912 this.loadWord(); 35913 return leadingZeroCount + this.skipLeadingZeros(); 35914 }; 35915 35916 // ():void 35917 this.skipUnsignedExpGolomb = function () { 35918 this.skipBits(1 + this.skipLeadingZeros()); 35919 }; 35920 35921 // ():void 35922 this.skipExpGolomb = function () { 35923 this.skipBits(1 + this.skipLeadingZeros()); 35924 }; 35925 35926 // ():uint 35927 this.readUnsignedExpGolomb = function () { 35928 var clz = this.skipLeadingZeros(); // :uint 35929 return this.readBits(clz + 1) - 1; 35930 }; 35931 35932 // ():int 35933 this.readExpGolomb = function () { 35934 var valu = this.readUnsignedExpGolomb(); // :int 35935 if (0x01 & valu) { 35936 // the number is odd if the low order bit is set 35937 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 35938 } 35939 return -1 * (valu >>> 1); // divide by two then make it negative 35940 }; 35941 35942 // Some convenience functions 35943 // :Boolean 35944 this.readBoolean = function () { 35945 return this.readBits(1) === 1; 35946 }; 35947 35948 // ():int 35949 this.readUnsignedByte = function () { 35950 return this.readBits(8); 35951 }; 35952 35953 this.loadWord(); 35954 }; 35955 35956 var expGolomb = ExpGolomb; 35957 35958 var _H264Stream, _NalByteStream; 35959 var PROFILES_WITH_OPTIONAL_SPS_DATA; 35960 35961 /** 35962 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 35963 */ 35964 _NalByteStream = function NalByteStream() { 35965 var syncPoint = 0, 35966 i, 35967 buffer; 35968 _NalByteStream.prototype.init.call(this); 35969 35970 this.push = function (data) { 35971 var swapBuffer; 35972 35973 if (!buffer) { 35974 buffer = data.data; 35975 } else { 35976 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 35977 swapBuffer.set(buffer); 35978 swapBuffer.set(data.data, buffer.byteLength); 35979 buffer = swapBuffer; 35980 } 35981 35982 // Rec. ITU-T H.264, Annex B 35983 // scan for NAL unit boundaries 35984 35985 // a match looks like this: 35986 // 0 0 1 .. NAL .. 0 0 1 35987 // ^ sync point ^ i 35988 // or this: 35989 // 0 0 1 .. NAL .. 0 0 0 35990 // ^ sync point ^ i 35991 35992 // advance the sync point to a NAL start, if necessary 35993 for (; syncPoint < buffer.byteLength - 3; syncPoint++) { 35994 if (buffer[syncPoint + 2] === 1) { 35995 // the sync point is properly aligned 35996 i = syncPoint + 5; 35997 break; 35998 } 35999 } 36000 36001 while (i < buffer.byteLength) { 36002 // look at the current byte to determine if we've hit the end of 36003 // a NAL unit boundary 36004 switch (buffer[i]) { 36005 case 0: 36006 // skip past non-sync sequences 36007 if (buffer[i - 1] !== 0) { 36008 i += 2; 36009 break; 36010 } else if (buffer[i - 2] !== 0) { 36011 i++; 36012 break; 36013 } 36014 36015 // deliver the NAL unit if it isn't empty 36016 if (syncPoint + 3 !== i - 2) { 36017 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 36018 } 36019 36020 // drop trailing zeroes 36021 do { 36022 i++; 36023 } while (buffer[i] !== 1 && i < buffer.length); 36024 syncPoint = i - 2; 36025 i += 3; 36026 break; 36027 case 1: 36028 // skip past non-sync sequences 36029 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 36030 i += 3; 36031 break; 36032 } 36033 36034 // deliver the NAL unit 36035 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 36036 syncPoint = i - 2; 36037 i += 3; 36038 break; 36039 default: 36040 // the current byte isn't a one or zero, so it cannot be part 36041 // of a sync sequence 36042 i += 3; 36043 break; 36044 } 36045 } 36046 // filter out the NAL units that were delivered 36047 buffer = buffer.subarray(syncPoint); 36048 i -= syncPoint; 36049 syncPoint = 0; 36050 }; 36051 36052 this.flush = function () { 36053 // deliver the last buffered NAL unit 36054 if (buffer && buffer.byteLength > 3) { 36055 this.trigger('data', buffer.subarray(syncPoint + 3)); 36056 } 36057 // reset the stream state 36058 buffer = null; 36059 syncPoint = 0; 36060 this.trigger('done'); 36061 }; 36062 }; 36063 _NalByteStream.prototype = new stream(); 36064 36065 // values of profile_idc that indicate additional fields are included in the SPS 36066 // see Recommendation ITU-T H.264 (4/2013), 36067 // 7.3.2.1.1 Sequence parameter set data syntax 36068 PROFILES_WITH_OPTIONAL_SPS_DATA = { 36069 100: true, 36070 110: true, 36071 122: true, 36072 244: true, 36073 44: true, 36074 83: true, 36075 86: true, 36076 118: true, 36077 128: true, 36078 138: true, 36079 139: true, 36080 134: true 36081 }; 36082 36083 /** 36084 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 36085 * events. 36086 */ 36087 _H264Stream = function H264Stream() { 36088 var nalByteStream = new _NalByteStream(), 36089 self, 36090 trackId, 36091 currentPts, 36092 currentDts, 36093 discardEmulationPreventionBytes, 36094 readSequenceParameterSet, 36095 skipScalingList; 36096 36097 _H264Stream.prototype.init.call(this); 36098 self = this; 36099 36100 this.push = function (packet) { 36101 if (packet.type !== 'video') { 36102 return; 36103 } 36104 trackId = packet.trackId; 36105 currentPts = packet.pts; 36106 currentDts = packet.dts; 36107 36108 nalByteStream.push(packet); 36109 }; 36110 36111 nalByteStream.on('data', function (data) { 36112 var event = { 36113 trackId: trackId, 36114 pts: currentPts, 36115 dts: currentDts, 36116 data: data 36117 }; 36118 36119 switch (data[0] & 0x1f) { 36120 case 0x05: 36121 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 36122 break; 36123 case 0x06: 36124 event.nalUnitType = 'sei_rbsp'; 36125 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 36126 break; 36127 case 0x07: 36128 event.nalUnitType = 'seq_parameter_set_rbsp'; 36129 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 36130 event.config = readSequenceParameterSet(event.escapedRBSP); 36131 break; 36132 case 0x08: 36133 event.nalUnitType = 'pic_parameter_set_rbsp'; 36134 break; 36135 case 0x09: 36136 event.nalUnitType = 'access_unit_delimiter_rbsp'; 36137 break; 36138 36139 default: 36140 break; 36141 } 36142 self.trigger('data', event); 36143 }); 36144 nalByteStream.on('done', function () { 36145 self.trigger('done'); 36146 }); 36147 36148 this.flush = function () { 36149 nalByteStream.flush(); 36150 }; 36151 36152 /** 36153 * Advance the ExpGolomb decoder past a scaling list. The scaling 36154 * list is optionally transmitted as part of a sequence parameter 36155 * set and is not relevant to transmuxing. 36156 * @param count {number} the number of entries in this scaling list 36157 * @param expGolombDecoder {object} an ExpGolomb pointed to the 36158 * start of a scaling list 36159 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 36160 */ 36161 skipScalingList = function skipScalingList(count, expGolombDecoder) { 36162 var lastScale = 8, 36163 nextScale = 8, 36164 j, 36165 deltaScale; 36166 36167 for (j = 0; j < count; j++) { 36168 if (nextScale !== 0) { 36169 deltaScale = expGolombDecoder.readExpGolomb(); 36170 nextScale = (lastScale + deltaScale + 256) % 256; 36171 } 36172 36173 lastScale = nextScale === 0 ? lastScale : nextScale; 36174 } 36175 }; 36176 36177 /** 36178 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 36179 * Sequence Payload" 36180 * @param data {Uint8Array} the bytes of a RBSP from a NAL 36181 * unit 36182 * @return {Uint8Array} the RBSP without any Emulation 36183 * Prevention Bytes 36184 */ 36185 discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) { 36186 var length = data.byteLength, 36187 emulationPreventionBytesPositions = [], 36188 i = 1, 36189 newLength, 36190 newData; 36191 36192 // Find all `Emulation Prevention Bytes` 36193 while (i < length - 2) { 36194 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 36195 emulationPreventionBytesPositions.push(i + 2); 36196 i += 2; 36197 } else { 36198 i++; 36199 } 36200 } 36201 36202 // If no Emulation Prevention Bytes were found just return the original 36203 // array 36204 if (emulationPreventionBytesPositions.length === 0) { 36205 return data; 36206 } 36207 36208 // Create a new array to hold the NAL unit data 36209 newLength = length - emulationPreventionBytesPositions.length; 36210 newData = new Uint8Array(newLength); 36211 var sourceIndex = 0; 36212 36213 for (i = 0; i < newLength; sourceIndex++, i++) { 36214 if (sourceIndex === emulationPreventionBytesPositions[0]) { 36215 // Skip this byte 36216 sourceIndex++; 36217 // Remove this position index 36218 emulationPreventionBytesPositions.shift(); 36219 } 36220 newData[i] = data[sourceIndex]; 36221 } 36222 36223 return newData; 36224 }; 36225 36226 /** 36227 * Read a sequence parameter set and return some interesting video 36228 * properties. A sequence parameter set is the H264 metadata that 36229 * describes the properties of upcoming video frames. 36230 * @param data {Uint8Array} the bytes of a sequence parameter set 36231 * @return {object} an object with configuration parsed from the 36232 * sequence parameter set, including the dimensions of the 36233 * associated video frames. 36234 */ 36235 readSequenceParameterSet = function readSequenceParameterSet(data) { 36236 var frameCropLeftOffset = 0, 36237 frameCropRightOffset = 0, 36238 frameCropTopOffset = 0, 36239 frameCropBottomOffset = 0, 36240 sarScale = 1, 36241 expGolombDecoder, 36242 profileIdc, 36243 levelIdc, 36244 profileCompatibility, 36245 chromaFormatIdc, 36246 picOrderCntType, 36247 numRefFramesInPicOrderCntCycle, 36248 picWidthInMbsMinus1, 36249 picHeightInMapUnitsMinus1, 36250 frameMbsOnlyFlag, 36251 scalingListCount, 36252 sarRatio, 36253 aspectRatioIdc, 36254 i; 36255 36256 expGolombDecoder = new expGolomb(data); 36257 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 36258 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 36259 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 36260 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 36261 36262 // some profiles have more optional data we don't need 36263 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 36264 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 36265 if (chromaFormatIdc === 3) { 36266 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 36267 } 36268 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 36269 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 36270 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 36271 if (expGolombDecoder.readBoolean()) { 36272 // seq_scaling_matrix_present_flag 36273 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 36274 for (i = 0; i < scalingListCount; i++) { 36275 if (expGolombDecoder.readBoolean()) { 36276 // seq_scaling_list_present_flag[ i ] 36277 if (i < 6) { 36278 skipScalingList(16, expGolombDecoder); 36279 } else { 36280 skipScalingList(64, expGolombDecoder); 36281 } 36282 } 36283 } 36284 } 36285 } 36286 36287 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 36288 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 36289 36290 if (picOrderCntType === 0) { 36291 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 36292 } else if (picOrderCntType === 1) { 36293 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 36294 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 36295 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 36296 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 36297 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 36298 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 36299 } 36300 } 36301 36302 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 36303 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 36304 36305 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 36306 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 36307 36308 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 36309 if (frameMbsOnlyFlag === 0) { 36310 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 36311 } 36312 36313 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 36314 if (expGolombDecoder.readBoolean()) { 36315 // frame_cropping_flag 36316 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 36317 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 36318 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 36319 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 36320 } 36321 if (expGolombDecoder.readBoolean()) { 36322 // vui_parameters_present_flag 36323 if (expGolombDecoder.readBoolean()) { 36324 // aspect_ratio_info_present_flag 36325 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 36326 switch (aspectRatioIdc) { 36327 case 1: 36328 sarRatio = [1, 1];break; 36329 case 2: 36330 sarRatio = [12, 11];break; 36331 case 3: 36332 sarRatio = [10, 11];break; 36333 case 4: 36334 sarRatio = [16, 11];break; 36335 case 5: 36336 sarRatio = [40, 33];break; 36337 case 6: 36338 sarRatio = [24, 11];break; 36339 case 7: 36340 sarRatio = [20, 11];break; 36341 case 8: 36342 sarRatio = [32, 11];break; 36343 case 9: 36344 sarRatio = [80, 33];break; 36345 case 10: 36346 sarRatio = [18, 11];break; 36347 case 11: 36348 sarRatio = [15, 11];break; 36349 case 12: 36350 sarRatio = [64, 33];break; 36351 case 13: 36352 sarRatio = [160, 99];break; 36353 case 14: 36354 sarRatio = [4, 3];break; 36355 case 15: 36356 sarRatio = [3, 2];break; 36357 case 16: 36358 sarRatio = [2, 1];break; 36359 case 255: 36360 { 36361 sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()]; 36362 break; 36363 } 36364 } 36365 if (sarRatio) { 36366 sarScale = sarRatio[0] / sarRatio[1]; 36367 } 36368 } 36369 } 36370 return { 36371 profileIdc: profileIdc, 36372 levelIdc: levelIdc, 36373 profileCompatibility: profileCompatibility, 36374 width: Math.ceil(((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), 36375 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2 36376 }; 36377 }; 36378 }; 36379 _H264Stream.prototype = new stream(); 36380 36381 var h264 = { 36382 H264Stream: _H264Stream, 36383 NalByteStream: _NalByteStream 36384 }; 36385 36386 // Constants 36387 var _AacStream; 36388 36389 /** 36390 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 36391 */ 36392 36393 _AacStream = function AacStream() { 36394 var everything = new Uint8Array(), 36395 timeStamp = 0; 36396 36397 _AacStream.prototype.init.call(this); 36398 36399 this.setTimestamp = function (timestamp) { 36400 timeStamp = timestamp; 36401 }; 36402 36403 this.parseId3TagSize = function (header, byteIndex) { 36404 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 36405 flags = header[byteIndex + 5], 36406 footerPresent = (flags & 16) >> 4; 36407 36408 if (footerPresent) { 36409 return returnSize + 20; 36410 } 36411 return returnSize + 10; 36412 }; 36413 36414 this.parseAdtsSize = function (header, byteIndex) { 36415 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 36416 middle = header[byteIndex + 4] << 3, 36417 highTwo = header[byteIndex + 3] & 0x3 << 11; 36418 36419 return highTwo | middle | lowThree; 36420 }; 36421 36422 this.push = function (bytes) { 36423 var frameSize = 0, 36424 byteIndex = 0, 36425 bytesLeft, 36426 chunk, 36427 packet, 36428 tempLength; 36429 36430 // If there are bytes remaining from the last segment, prepend them to the 36431 // bytes that were pushed in 36432 if (everything.length) { 36433 tempLength = everything.length; 36434 everything = new Uint8Array(bytes.byteLength + tempLength); 36435 everything.set(everything.subarray(0, tempLength)); 36436 everything.set(bytes, tempLength); 36437 } else { 36438 everything = bytes; 36439 } 36440 36441 while (everything.length - byteIndex >= 3) { 36442 if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) { 36443 36444 // Exit early because we don't have enough to parse 36445 // the ID3 tag header 36446 if (everything.length - byteIndex < 10) { 36447 break; 36448 } 36449 36450 // check framesize 36451 frameSize = this.parseId3TagSize(everything, byteIndex); 36452 36453 // Exit early if we don't have enough in the buffer 36454 // to emit a full packet 36455 if (frameSize > everything.length) { 36456 break; 36457 } 36458 chunk = { 36459 type: 'timed-metadata', 36460 data: everything.subarray(byteIndex, byteIndex + frameSize) 36461 }; 36462 this.trigger('data', chunk); 36463 byteIndex += frameSize; 36464 continue; 36465 } else if (everything[byteIndex] & 0xff === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) { 36466 36467 // Exit early because we don't have enough to parse 36468 // the ADTS frame header 36469 if (everything.length - byteIndex < 7) { 36470 break; 36471 } 36472 36473 frameSize = this.parseAdtsSize(everything, byteIndex); 36474 36475 // Exit early if we don't have enough in the buffer 36476 // to emit a full packet 36477 if (frameSize > everything.length) { 36478 break; 36479 } 36480 36481 packet = { 36482 type: 'audio', 36483 data: everything.subarray(byteIndex, byteIndex + frameSize), 36484 pts: timeStamp, 36485 dts: timeStamp 36486 }; 36487 this.trigger('data', packet); 36488 byteIndex += frameSize; 36489 continue; 36490 } 36491 byteIndex++; 36492 } 36493 bytesLeft = everything.length - byteIndex; 36494 36495 if (bytesLeft > 0) { 36496 everything = everything.subarray(byteIndex); 36497 } else { 36498 everything = new Uint8Array(); 36499 } 36500 }; 36501 }; 36502 36503 _AacStream.prototype = new stream(); 36504 36505 var aac = _AacStream; 36506 36507 var highPrefix = [33, 16, 5, 32, 164, 27]; 36508 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 36509 var zeroFill = function zeroFill(count) { 36510 var a = []; 36511 while (count--) { 36512 a.push(0); 36513 } 36514 return a; 36515 }; 36516 36517 var makeTable = function makeTable(metaTable) { 36518 return Object.keys(metaTable).reduce(function (obj, key) { 36519 obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) { 36520 return arr.concat(part); 36521 }, [])); 36522 return obj; 36523 }, {}); 36524 }; 36525 36526 // Frames-of-silence to use for filling in missing AAC frames 36527 var coneOfSilence = { 36528 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 36529 88200: [highPrefix, [231], zeroFill(170), [56]], 36530 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 36531 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 36532 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 36533 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 36534 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]], 36535 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]], 36536 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]], 36537 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]], 36538 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]] 36539 }; 36540 36541 var silence = makeTable(coneOfSilence); 36542 36543 var ONE_SECOND_IN_TS = 90000, 36544 36545 36546 // 90kHz clock 36547 secondsToVideoTs, 36548 secondsToAudioTs, 36549 videoTsToSeconds, 36550 audioTsToSeconds, 36551 audioTsToVideoTs, 36552 videoTsToAudioTs; 36553 36554 secondsToVideoTs = function secondsToVideoTs(seconds) { 36555 return seconds * ONE_SECOND_IN_TS; 36556 }; 36557 36558 secondsToAudioTs = function secondsToAudioTs(seconds, sampleRate) { 36559 return seconds * sampleRate; 36560 }; 36561 36562 videoTsToSeconds = function videoTsToSeconds(timestamp) { 36563 return timestamp / ONE_SECOND_IN_TS; 36564 }; 36565 36566 audioTsToSeconds = function audioTsToSeconds(timestamp, sampleRate) { 36567 return timestamp / sampleRate; 36568 }; 36569 36570 audioTsToVideoTs = function audioTsToVideoTs(timestamp, sampleRate) { 36571 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 36572 }; 36573 36574 videoTsToAudioTs = function videoTsToAudioTs(timestamp, sampleRate) { 36575 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 36576 }; 36577 36578 var clock = { 36579 secondsToVideoTs: secondsToVideoTs, 36580 secondsToAudioTs: secondsToAudioTs, 36581 videoTsToSeconds: videoTsToSeconds, 36582 audioTsToSeconds: audioTsToSeconds, 36583 audioTsToVideoTs: audioTsToVideoTs, 36584 videoTsToAudioTs: videoTsToAudioTs 36585 }; 36586 36587 var H264Stream = h264.H264Stream; 36588 36589 // constants 36590 var AUDIO_PROPERTIES = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize']; 36591 36592 var VIDEO_PROPERTIES = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility']; 36593 36594 var ONE_SECOND_IN_TS$1 = 90000; // 90kHz clock 36595 36596 // object types 36597 var _VideoSegmentStream, _AudioSegmentStream, _Transmuxer, _CoalesceStream; 36598 36599 // Helper functions 36600 var createDefaultSample, isLikelyAacData, collectDtsInfo, clearDtsInfo, calculateTrackBaseMediaDecodeTime, arrayEquals, sumFrameByteLengths; 36601 36602 /** 36603 * Default sample object 36604 * see ISO/IEC 14496-12:2012, section 8.6.4.3 36605 */ 36606 createDefaultSample = function createDefaultSample() { 36607 return { 36608 size: 0, 36609 flags: { 36610 isLeading: 0, 36611 dependsOn: 1, 36612 isDependedOn: 0, 36613 hasRedundancy: 0, 36614 degradationPriority: 0 36615 } 36616 }; 36617 }; 36618 36619 isLikelyAacData = function isLikelyAacData(data) { 36620 if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) { 36621 return true; 36622 } 36623 return false; 36624 }; 36625 36626 /** 36627 * Compare two arrays (even typed) for same-ness 36628 */ 36629 arrayEquals = function arrayEquals(a, b) { 36630 var i; 36631 36632 if (a.length !== b.length) { 36633 return false; 36634 } 36635 36636 // compare the value of each element in the array 36637 for (i = 0; i < a.length; i++) { 36638 if (a[i] !== b[i]) { 36639 return false; 36640 } 36641 } 36642 36643 return true; 36644 }; 36645 36646 /** 36647 * Sum the `byteLength` properties of the data in each AAC frame 36648 */ 36649 sumFrameByteLengths = function sumFrameByteLengths(array) { 36650 var i, 36651 currentObj, 36652 sum = 0; 36653 36654 // sum the byteLength's all each nal unit in the frame 36655 for (i = 0; i < array.length; i++) { 36656 currentObj = array[i]; 36657 sum += currentObj.data.byteLength; 36658 } 36659 36660 return sum; 36661 }; 36662 36663 /** 36664 * Constructs a single-track, ISO BMFF media segment from AAC data 36665 * events. The output of this stream can be fed to a SourceBuffer 36666 * configured with a suitable initialization segment. 36667 * @param track {object} track metadata configuration 36668 * @param options {object} transmuxer options object 36669 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 36670 * in the source; false to adjust the first segment to start at 0. 36671 */ 36672 _AudioSegmentStream = function AudioSegmentStream(track, options) { 36673 var adtsFrames = [], 36674 sequenceNumber = 0, 36675 earliestAllowedDts = 0, 36676 audioAppendStartTs = 0, 36677 videoBaseMediaDecodeTime = Infinity; 36678 36679 options = options || {}; 36680 36681 _AudioSegmentStream.prototype.init.call(this); 36682 36683 this.push = function (data) { 36684 collectDtsInfo(track, data); 36685 36686 if (track) { 36687 AUDIO_PROPERTIES.forEach(function (prop) { 36688 track[prop] = data[prop]; 36689 }); 36690 } 36691 36692 // buffer audio data until end() is called 36693 adtsFrames.push(data); 36694 }; 36695 36696 this.setEarliestDts = function (earliestDts) { 36697 earliestAllowedDts = earliestDts - track.timelineStartInfo.baseMediaDecodeTime; 36698 }; 36699 36700 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 36701 videoBaseMediaDecodeTime = baseMediaDecodeTime; 36702 }; 36703 36704 this.setAudioAppendStart = function (timestamp) { 36705 audioAppendStartTs = timestamp; 36706 }; 36707 36708 this.flush = function () { 36709 var frames, moof, mdat, boxes; 36710 36711 // return early if no audio data has been observed 36712 if (adtsFrames.length === 0) { 36713 this.trigger('done', 'AudioSegmentStream'); 36714 return; 36715 } 36716 36717 frames = this.trimAdtsFramesByEarliestDts_(adtsFrames); 36718 track.baseMediaDecodeTime = calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 36719 36720 this.prefixWithSilence_(track, frames); 36721 36722 // we have to build the index from byte locations to 36723 // samples (that is, adts frames) in the audio data 36724 track.samples = this.generateSampleTable_(frames); 36725 36726 // concatenate the audio data to constuct the mdat 36727 mdat = mp4Generator.mdat(this.concatenateFrameData_(frames)); 36728 36729 adtsFrames = []; 36730 36731 moof = mp4Generator.moof(sequenceNumber, [track]); 36732 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); 36733 36734 // bump the sequence number for next time 36735 sequenceNumber++; 36736 36737 boxes.set(moof); 36738 boxes.set(mdat, moof.byteLength); 36739 36740 clearDtsInfo(track); 36741 36742 this.trigger('data', { track: track, boxes: boxes }); 36743 this.trigger('done', 'AudioSegmentStream'); 36744 }; 36745 36746 // Possibly pad (prefix) the audio track with silence if appending this track 36747 // would lead to the introduction of a gap in the audio buffer 36748 this.prefixWithSilence_ = function (track, frames) { 36749 var baseMediaDecodeTimeTs, 36750 frameDuration = 0, 36751 audioGapDuration = 0, 36752 audioFillFrameCount = 0, 36753 audioFillDuration = 0, 36754 silentFrame, 36755 i; 36756 36757 if (!frames.length) { 36758 return; 36759 } 36760 36761 baseMediaDecodeTimeTs = clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); 36762 // determine frame clock duration based on sample rate, round up to avoid overfills 36763 frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 / (track.samplerate / 1024)); 36764 36765 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 36766 // insert the shortest possible amount (audio gap or audio to video gap) 36767 audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); 36768 // number of full frames in the audio gap 36769 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 36770 audioFillDuration = audioFillFrameCount * frameDuration; 36771 } 36772 36773 // don't attempt to fill gaps smaller than a single frame or larger 36774 // than a half second 36775 if (audioFillFrameCount < 1 || audioFillDuration > ONE_SECOND_IN_TS$1 / 2) { 36776 return; 36777 } 36778 36779 silentFrame = silence[track.samplerate]; 36780 36781 if (!silentFrame) { 36782 // we don't have a silent frame pregenerated for the sample rate, so use a frame 36783 // from the content instead 36784 silentFrame = frames[0].data; 36785 } 36786 36787 for (i = 0; i < audioFillFrameCount; i++) { 36788 frames.splice(i, 0, { 36789 data: silentFrame 36790 }); 36791 } 36792 36793 track.baseMediaDecodeTime -= Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate)); 36794 }; 36795 36796 // If the audio segment extends before the earliest allowed dts 36797 // value, remove AAC frames until starts at or after the earliest 36798 // allowed DTS so that we don't end up with a negative baseMedia- 36799 // DecodeTime for the audio track 36800 this.trimAdtsFramesByEarliestDts_ = function (adtsFrames) { 36801 if (track.minSegmentDts >= earliestAllowedDts) { 36802 return adtsFrames; 36803 } 36804 36805 // We will need to recalculate the earliest segment Dts 36806 track.minSegmentDts = Infinity; 36807 36808 return adtsFrames.filter(function (currentFrame) { 36809 // If this is an allowed frame, keep it and record it's Dts 36810 if (currentFrame.dts >= earliestAllowedDts) { 36811 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 36812 track.minSegmentPts = track.minSegmentDts; 36813 return true; 36814 } 36815 // Otherwise, discard it 36816 return false; 36817 }); 36818 }; 36819 36820 // generate the track's raw mdat data from an array of frames 36821 this.generateSampleTable_ = function (frames) { 36822 var i, 36823 currentFrame, 36824 samples = []; 36825 36826 for (i = 0; i < frames.length; i++) { 36827 currentFrame = frames[i]; 36828 samples.push({ 36829 size: currentFrame.data.byteLength, 36830 duration: 1024 // For AAC audio, all samples contain 1024 samples 36831 }); 36832 } 36833 return samples; 36834 }; 36835 36836 // generate the track's sample table from an array of frames 36837 this.concatenateFrameData_ = function (frames) { 36838 var i, 36839 currentFrame, 36840 dataOffset = 0, 36841 data = new Uint8Array(sumFrameByteLengths(frames)); 36842 36843 for (i = 0; i < frames.length; i++) { 36844 currentFrame = frames[i]; 36845 36846 data.set(currentFrame.data, dataOffset); 36847 dataOffset += currentFrame.data.byteLength; 36848 } 36849 return data; 36850 }; 36851 }; 36852 36853 _AudioSegmentStream.prototype = new stream(); 36854 36855 /** 36856 * Constructs a single-track, ISO BMFF media segment from H264 data 36857 * events. The output of this stream can be fed to a SourceBuffer 36858 * configured with a suitable initialization segment. 36859 * @param track {object} track metadata configuration 36860 * @param options {object} transmuxer options object 36861 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 36862 * gopsToAlignWith list when attempting to align gop pts 36863 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 36864 * in the source; false to adjust the first segment to start at 0. 36865 */ 36866 _VideoSegmentStream = function VideoSegmentStream(track, options) { 36867 var sequenceNumber = 0, 36868 nalUnits = [], 36869 gopsToAlignWith = [], 36870 config, 36871 pps; 36872 36873 options = options || {}; 36874 36875 _VideoSegmentStream.prototype.init.call(this); 36876 36877 delete track.minPTS; 36878 36879 this.gopCache_ = []; 36880 36881 this.push = function (nalUnit) { 36882 collectDtsInfo(track, nalUnit); 36883 36884 // record the track config 36885 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 36886 config = nalUnit.config; 36887 track.sps = [nalUnit.data]; 36888 36889 VIDEO_PROPERTIES.forEach(function (prop) { 36890 track[prop] = config[prop]; 36891 }, this); 36892 } 36893 36894 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 36895 pps = nalUnit.data; 36896 track.pps = [nalUnit.data]; 36897 } 36898 36899 // buffer video until flush() is called 36900 nalUnits.push(nalUnit); 36901 }; 36902 36903 this.flush = function () { 36904 var frames, gopForFusion, gops, moof, mdat, boxes; 36905 36906 // Throw away nalUnits at the start of the byte stream until 36907 // we find the first AUD 36908 while (nalUnits.length) { 36909 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 36910 break; 36911 } 36912 nalUnits.shift(); 36913 } 36914 36915 // Return early if no video data has been observed 36916 if (nalUnits.length === 0) { 36917 this.resetStream_(); 36918 this.trigger('done', 'VideoSegmentStream'); 36919 return; 36920 } 36921 36922 // Organize the raw nal-units into arrays that represent 36923 // higher-level constructs such as frames and gops 36924 // (group-of-pictures) 36925 frames = this.groupNalsIntoFrames_(nalUnits); 36926 gops = this.groupFramesIntoGops_(frames); 36927 36928 // If the first frame of this fragment is not a keyframe we have 36929 // a problem since MSE (on Chrome) requires a leading keyframe. 36930 // 36931 // We have two approaches to repairing this situation: 36932 // 1) GOP-FUSION: 36933 // This is where we keep track of the GOPS (group-of-pictures) 36934 // from previous fragments and attempt to find one that we can 36935 // prepend to the current fragment in order to create a valid 36936 // fragment. 36937 // 2) KEYFRAME-PULLING: 36938 // Here we search for the first keyframe in the fragment and 36939 // throw away all the frames between the start of the fragment 36940 // and that keyframe. We then extend the duration and pull the 36941 // PTS of the keyframe forward so that it covers the time range 36942 // of the frames that were disposed of. 36943 // 36944 // #1 is far prefereable over #2 which can cause "stuttering" but 36945 // requires more things to be just right. 36946 if (!gops[0][0].keyFrame) { 36947 // Search for a gop for fusion from our gopCache 36948 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 36949 36950 if (gopForFusion) { 36951 gops.unshift(gopForFusion); 36952 // Adjust Gops' metadata to account for the inclusion of the 36953 // new gop at the beginning 36954 gops.byteLength += gopForFusion.byteLength; 36955 gops.nalCount += gopForFusion.nalCount; 36956 gops.pts = gopForFusion.pts; 36957 gops.dts = gopForFusion.dts; 36958 gops.duration += gopForFusion.duration; 36959 } else { 36960 // If we didn't find a candidate gop fall back to keyframe-pulling 36961 gops = this.extendFirstKeyFrame_(gops); 36962 } 36963 } 36964 36965 // Trim gops to align with gopsToAlignWith 36966 if (gopsToAlignWith.length) { 36967 var alignedGops; 36968 36969 if (options.alignGopsAtEnd) { 36970 alignedGops = this.alignGopsAtEnd_(gops); 36971 } else { 36972 alignedGops = this.alignGopsAtStart_(gops); 36973 } 36974 36975 if (!alignedGops) { 36976 // save all the nals in the last GOP into the gop cache 36977 this.gopCache_.unshift({ 36978 gop: gops.pop(), 36979 pps: track.pps, 36980 sps: track.sps 36981 }); 36982 36983 // Keep a maximum of 6 GOPs in the cache 36984 this.gopCache_.length = Math.min(6, this.gopCache_.length); 36985 36986 // Clear nalUnits 36987 nalUnits = []; 36988 36989 // return early no gops can be aligned with desired gopsToAlignWith 36990 this.resetStream_(); 36991 this.trigger('done', 'VideoSegmentStream'); 36992 return; 36993 } 36994 36995 // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 36996 // when recalculated before sending off to CoalesceStream 36997 clearDtsInfo(track); 36998 36999 gops = alignedGops; 37000 } 37001 37002 collectDtsInfo(track, gops); 37003 37004 // First, we have to build the index from byte locations to 37005 // samples (that is, frames) in the video data 37006 track.samples = this.generateSampleTable_(gops); 37007 37008 // Concatenate the video data and construct the mdat 37009 mdat = mp4Generator.mdat(this.concatenateNalData_(gops)); 37010 37011 track.baseMediaDecodeTime = calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 37012 37013 this.trigger('processedGopsInfo', gops.map(function (gop) { 37014 return { 37015 pts: gop.pts, 37016 dts: gop.dts, 37017 byteLength: gop.byteLength 37018 }; 37019 })); 37020 37021 // save all the nals in the last GOP into the gop cache 37022 this.gopCache_.unshift({ 37023 gop: gops.pop(), 37024 pps: track.pps, 37025 sps: track.sps 37026 }); 37027 37028 // Keep a maximum of 6 GOPs in the cache 37029 this.gopCache_.length = Math.min(6, this.gopCache_.length); 37030 37031 // Clear nalUnits 37032 nalUnits = []; 37033 37034 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 37035 this.trigger('timelineStartInfo', track.timelineStartInfo); 37036 37037 moof = mp4Generator.moof(sequenceNumber, [track]); 37038 37039 // it would be great to allocate this array up front instead of 37040 // throwing away hundreds of media segment fragments 37041 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); 37042 37043 // Bump the sequence number for next time 37044 sequenceNumber++; 37045 37046 boxes.set(moof); 37047 boxes.set(mdat, moof.byteLength); 37048 37049 this.trigger('data', { track: track, boxes: boxes }); 37050 37051 this.resetStream_(); 37052 37053 // Continue with the flush process now 37054 this.trigger('done', 'VideoSegmentStream'); 37055 }; 37056 37057 this.resetStream_ = function () { 37058 clearDtsInfo(track); 37059 37060 // reset config and pps because they may differ across segments 37061 // for instance, when we are rendition switching 37062 config = undefined; 37063 pps = undefined; 37064 }; 37065 37066 // Search for a candidate Gop for gop-fusion from the gop cache and 37067 // return it or return null if no good candidate was found 37068 this.getGopForFusion_ = function (nalUnit) { 37069 var halfSecond = 45000, 37070 37071 37072 // Half-a-second in a 90khz clock 37073 allowableOverlap = 10000, 37074 37075 37076 // About 3 frames @ 30fps 37077 nearestDistance = Infinity, 37078 dtsDistance, 37079 nearestGopObj, 37080 currentGop, 37081 currentGopObj, 37082 i; 37083 37084 // Search for the GOP nearest to the beginning of this nal unit 37085 for (i = 0; i < this.gopCache_.length; i++) { 37086 currentGopObj = this.gopCache_[i]; 37087 currentGop = currentGopObj.gop; 37088 37089 // Reject Gops with different SPS or PPS 37090 if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) { 37091 continue; 37092 } 37093 37094 // Reject Gops that would require a negative baseMediaDecodeTime 37095 if (currentGop.dts < track.timelineStartInfo.dts) { 37096 continue; 37097 } 37098 37099 // The distance between the end of the gop and the start of the nalUnit 37100 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; 37101 37102 // Only consider GOPS that start before the nal unit and end within 37103 // a half-second of the nal unit 37104 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 37105 37106 // Always use the closest GOP we found if there is more than 37107 // one candidate 37108 if (!nearestGopObj || nearestDistance > dtsDistance) { 37109 nearestGopObj = currentGopObj; 37110 nearestDistance = dtsDistance; 37111 } 37112 } 37113 } 37114 37115 if (nearestGopObj) { 37116 return nearestGopObj.gop; 37117 } 37118 return null; 37119 }; 37120 37121 this.extendFirstKeyFrame_ = function (gops) { 37122 var currentGop; 37123 37124 if (!gops[0][0].keyFrame && gops.length > 1) { 37125 // Remove the first GOP 37126 currentGop = gops.shift(); 37127 37128 gops.byteLength -= currentGop.byteLength; 37129 gops.nalCount -= currentGop.nalCount; 37130 37131 // Extend the first frame of what is now the 37132 // first gop to cover the time period of the 37133 // frames we just removed 37134 gops[0][0].dts = currentGop.dts; 37135 gops[0][0].pts = currentGop.pts; 37136 gops[0][0].duration += currentGop.duration; 37137 } 37138 37139 return gops; 37140 }; 37141 37142 // Convert an array of nal units into an array of frames with each frame being 37143 // composed of the nal units that make up that frame 37144 // Also keep track of cummulative data about the frame from the nal units such 37145 // as the frame duration, starting pts, etc. 37146 this.groupNalsIntoFrames_ = function (nalUnits) { 37147 var i, 37148 currentNal, 37149 currentFrame = [], 37150 frames = []; 37151 37152 currentFrame.byteLength = 0; 37153 37154 for (i = 0; i < nalUnits.length; i++) { 37155 currentNal = nalUnits[i]; 37156 37157 // Split on 'aud'-type nal units 37158 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 37159 // Since the very first nal unit is expected to be an AUD 37160 // only push to the frames array when currentFrame is not empty 37161 if (currentFrame.length) { 37162 currentFrame.duration = currentNal.dts - currentFrame.dts; 37163 frames.push(currentFrame); 37164 } 37165 currentFrame = [currentNal]; 37166 currentFrame.byteLength = currentNal.data.byteLength; 37167 currentFrame.pts = currentNal.pts; 37168 currentFrame.dts = currentNal.dts; 37169 } else { 37170 // Specifically flag key frames for ease of use later 37171 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 37172 currentFrame.keyFrame = true; 37173 } 37174 currentFrame.duration = currentNal.dts - currentFrame.dts; 37175 currentFrame.byteLength += currentNal.data.byteLength; 37176 currentFrame.push(currentNal); 37177 } 37178 } 37179 37180 // For the last frame, use the duration of the previous frame if we 37181 // have nothing better to go on 37182 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 37183 currentFrame.duration = frames[frames.length - 1].duration; 37184 } 37185 37186 // Push the final frame 37187 frames.push(currentFrame); 37188 return frames; 37189 }; 37190 37191 // Convert an array of frames into an array of Gop with each Gop being composed 37192 // of the frames that make up that Gop 37193 // Also keep track of cummulative data about the Gop from the frames such as the 37194 // Gop duration, starting pts, etc. 37195 this.groupFramesIntoGops_ = function (frames) { 37196 var i, 37197 currentFrame, 37198 currentGop = [], 37199 gops = []; 37200 37201 // We must pre-set some of the values on the Gop since we 37202 // keep running totals of these values 37203 currentGop.byteLength = 0; 37204 currentGop.nalCount = 0; 37205 currentGop.duration = 0; 37206 currentGop.pts = frames[0].pts; 37207 currentGop.dts = frames[0].dts; 37208 37209 // store some metadata about all the Gops 37210 gops.byteLength = 0; 37211 gops.nalCount = 0; 37212 gops.duration = 0; 37213 gops.pts = frames[0].pts; 37214 gops.dts = frames[0].dts; 37215 37216 for (i = 0; i < frames.length; i++) { 37217 currentFrame = frames[i]; 37218 37219 if (currentFrame.keyFrame) { 37220 // Since the very first frame is expected to be an keyframe 37221 // only push to the gops array when currentGop is not empty 37222 if (currentGop.length) { 37223 gops.push(currentGop); 37224 gops.byteLength += currentGop.byteLength; 37225 gops.nalCount += currentGop.nalCount; 37226 gops.duration += currentGop.duration; 37227 } 37228 37229 currentGop = [currentFrame]; 37230 currentGop.nalCount = currentFrame.length; 37231 currentGop.byteLength = currentFrame.byteLength; 37232 currentGop.pts = currentFrame.pts; 37233 currentGop.dts = currentFrame.dts; 37234 currentGop.duration = currentFrame.duration; 37235 } else { 37236 currentGop.duration += currentFrame.duration; 37237 currentGop.nalCount += currentFrame.length; 37238 currentGop.byteLength += currentFrame.byteLength; 37239 currentGop.push(currentFrame); 37240 } 37241 } 37242 37243 if (gops.length && currentGop.duration <= 0) { 37244 currentGop.duration = gops[gops.length - 1].duration; 37245 } 37246 gops.byteLength += currentGop.byteLength; 37247 gops.nalCount += currentGop.nalCount; 37248 gops.duration += currentGop.duration; 37249 37250 // push the final Gop 37251 gops.push(currentGop); 37252 return gops; 37253 }; 37254 37255 // generate the track's sample table from an array of gops 37256 this.generateSampleTable_ = function (gops, baseDataOffset) { 37257 var h, 37258 i, 37259 sample, 37260 currentGop, 37261 currentFrame, 37262 dataOffset = baseDataOffset || 0, 37263 samples = []; 37264 37265 for (h = 0; h < gops.length; h++) { 37266 currentGop = gops[h]; 37267 37268 for (i = 0; i < currentGop.length; i++) { 37269 currentFrame = currentGop[i]; 37270 37271 sample = createDefaultSample(); 37272 37273 sample.dataOffset = dataOffset; 37274 sample.compositionTimeOffset = currentFrame.pts - currentFrame.dts; 37275 sample.duration = currentFrame.duration;
vendor: 4,205 bytes, lines 37276-37405
37276 sample.size = 4 * currentFrame.length; // Space for nal unit size 37277 sample.size += currentFrame.byteLength; 37278 37279 if (currentFrame.keyFrame) { 37280 sample.flags.dependsOn = 2; 37281 } 37282 37283 dataOffset += sample.size; 37284 37285 samples.push(sample); 37286 } 37287 } 37288 return samples; 37289 }; 37290 37291 // generate the track's raw mdat data from an array of gops 37292 this.concatenateNalData_ = function (gops) { 37293 var h, 37294 i, 37295 j, 37296 currentGop, 37297 currentFrame, 37298 currentNal, 37299 dataOffset = 0, 37300 nalsByteLength = gops.byteLength, 37301 numberOfNals = gops.nalCount, 37302 totalByteLength = nalsByteLength + 4 * numberOfNals, 37303 data = new Uint8Array(totalByteLength), 37304 view = new DataView(data.buffer); 37305 37306 // For each Gop.. 37307 for (h = 0; h < gops.length; h++) { 37308 currentGop = gops[h]; 37309 37310 // For each Frame.. 37311 for (i = 0; i < currentGop.length; i++) { 37312 currentFrame = currentGop[i]; 37313 37314 // For each NAL.. 37315 for (j = 0; j < currentFrame.length; j++) { 37316 currentNal = currentFrame[j]; 37317 37318 view.setUint32(dataOffset, currentNal.data.byteLength); 37319 dataOffset += 4; 37320 data.set(currentNal.data, dataOffset); 37321 dataOffset += currentNal.data.byteLength; 37322 } 37323 } 37324 } 37325 return data; 37326 }; 37327 37328 // trim gop list to the first gop found that has a matching pts with a gop in the list 37329 // of gopsToAlignWith starting from the START of the list 37330 this.alignGopsAtStart_ = function (gops) { 37331 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 37332 37333 byteLength = gops.byteLength; 37334 nalCount = gops.nalCount; 37335 duration = gops.duration; 37336 alignIndex = gopIndex = 0; 37337 37338 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 37339 align = gopsToAlignWith[alignIndex]; 37340 gop = gops[gopIndex]; 37341 37342 if (align.pts === gop.pts) { 37343 break; 37344 } 37345 37346 if (gop.pts > align.pts) { 37347 // this current gop starts after the current gop we want to align on, so increment 37348 // align index 37349 alignIndex++; 37350 continue; 37351 } 37352 37353 // current gop starts before the current gop we want to align on. so increment gop 37354 // index 37355 gopIndex++; 37356 byteLength -= gop.byteLength; 37357 nalCount -= gop.nalCount; 37358 duration -= gop.duration; 37359 } 37360 37361 if (gopIndex === 0) { 37362 // no gops to trim 37363 return gops; 37364 } 37365 37366 if (gopIndex === gops.length) { 37367 // all gops trimmed, skip appending all gops 37368 return null; 37369 } 37370 37371 alignedGops = gops.slice(gopIndex); 37372 alignedGops.byteLength = byteLength; 37373 alignedGops.duration = duration; 37374 alignedGops.nalCount = nalCount; 37375 alignedGops.pts = alignedGops[0].pts; 37376 alignedGops.dts = alignedGops[0].dts; 37377 37378 return alignedGops; 37379 }; 37380 37381 // trim gop list to the first gop found that has a matching pts with a gop in the list 37382 // of gopsToAlignWith starting from the END of the list 37383 this.alignGopsAtEnd_ = function (gops) { 37384 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 37385 37386 alignIndex = gopsToAlignWith.length - 1; 37387 gopIndex = gops.length - 1; 37388 alignEndIndex = null; 37389 matchFound = false; 37390 37391 while (alignIndex >= 0 && gopIndex >= 0) { 37392 align = gopsToAlignWith[alignIndex]; 37393 gop = gops[gopIndex]; 37394 37395 if (align.pts === gop.pts) { 37396 matchFound = true; 37397 break; 37398 } 37399 37400 if (align.pts > gop.pts) { 37401 alignIndex--; 37402 continue; 37403 } 37404 37405 if (alignIndex === gopsToAlignWith.length - 1) {
vendor: 310,676 bytes, lines 37406-46331
37406 // gop.pts is greater than the last alignment candidate. If no match is found 37407 // by the end of this loop, we still want to append gops that come after this 37408 // point 37409 alignEndIndex = gopIndex; 37410 } 37411 37412 gopIndex--; 37413 } 37414 37415 if (!matchFound && alignEndIndex === null) { 37416 return null; 37417 } 37418 37419 var trimIndex; 37420 37421 if (matchFound) { 37422 trimIndex = gopIndex; 37423 } else { 37424 trimIndex = alignEndIndex; 37425 } 37426 37427 if (trimIndex === 0) { 37428 return gops; 37429 } 37430 37431 var alignedGops = gops.slice(trimIndex); 37432 var metadata = alignedGops.reduce(function (total, gop) { 37433 total.byteLength += gop.byteLength; 37434 total.duration += gop.duration; 37435 total.nalCount += gop.nalCount; 37436 return total; 37437 }, { byteLength: 0, duration: 0, nalCount: 0 }); 37438 37439 alignedGops.byteLength = metadata.byteLength; 37440 alignedGops.duration = metadata.duration; 37441 alignedGops.nalCount = metadata.nalCount; 37442 alignedGops.pts = alignedGops[0].pts; 37443 alignedGops.dts = alignedGops[0].dts; 37444 37445 return alignedGops; 37446 }; 37447 37448 this.alignGopsWith = function (newGopsToAlignWith) { 37449 gopsToAlignWith = newGopsToAlignWith; 37450 }; 37451 }; 37452 37453 _VideoSegmentStream.prototype = new stream(); 37454 37455 /** 37456 * Store information about the start and end of the track and the 37457 * duration for each frame/sample we process in order to calculate 37458 * the baseMediaDecodeTime 37459 */ 37460 collectDtsInfo = function collectDtsInfo(track, data) { 37461 if (typeof data.pts === 'number') { 37462 if (track.timelineStartInfo.pts === undefined) { 37463 track.timelineStartInfo.pts = data.pts; 37464 } 37465 37466 if (track.minSegmentPts === undefined) { 37467 track.minSegmentPts = data.pts; 37468 } else { 37469 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 37470 } 37471 37472 if (track.maxSegmentPts === undefined) { 37473 track.maxSegmentPts = data.pts; 37474 } else { 37475 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 37476 } 37477 } 37478 37479 if (typeof data.dts === 'number') { 37480 if (track.timelineStartInfo.dts === undefined) { 37481 track.timelineStartInfo.dts = data.dts; 37482 } 37483 37484 if (track.minSegmentDts === undefined) { 37485 track.minSegmentDts = data.dts; 37486 } else { 37487 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 37488 } 37489 37490 if (track.maxSegmentDts === undefined) { 37491 track.maxSegmentDts = data.dts; 37492 } else { 37493 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 37494 } 37495 } 37496 }; 37497 37498 /** 37499 * Clear values used to calculate the baseMediaDecodeTime between 37500 * tracks 37501 */ 37502 clearDtsInfo = function clearDtsInfo(track) { 37503 delete track.minSegmentDts; 37504 delete track.maxSegmentDts; 37505 delete track.minSegmentPts; 37506 delete track.maxSegmentPts; 37507 }; 37508 37509 /** 37510 * Calculate the track's baseMediaDecodeTime based on the earliest 37511 * DTS the transmuxer has ever seen and the minimum DTS for the 37512 * current track 37513 * @param track {object} track metadata configuration 37514 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 37515 * in the source; false to adjust the first segment to start at 0. 37516 */ 37517 calculateTrackBaseMediaDecodeTime = function calculateTrackBaseMediaDecodeTime(track, keepOriginalTimestamps) { 37518 var baseMediaDecodeTime, 37519 scale, 37520 minSegmentDts = track.minSegmentDts; 37521 37522 // Optionally adjust the time so the first segment starts at zero. 37523 if (!keepOriginalTimestamps) { 37524 minSegmentDts -= track.timelineStartInfo.dts; 37525 } 37526 37527 // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 37528 // we want the start of the first segment to be placed 37529 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; 37530 37531 // Add to that the distance this segment is from the very first 37532 baseMediaDecodeTime += minSegmentDts; 37533 37534 // baseMediaDecodeTime must not become negative 37535 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 37536 37537 if (track.type === 'audio') { 37538 // Audio has a different clock equal to the sampling_rate so we need to 37539 // scale the PTS values into the clock rate of the track 37540 scale = track.samplerate / ONE_SECOND_IN_TS$1; 37541 baseMediaDecodeTime *= scale; 37542 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 37543 } 37544 37545 return baseMediaDecodeTime; 37546 }; 37547 37548 /** 37549 * A Stream that can combine multiple streams (ie. audio & video) 37550 * into a single output segment for MSE. Also supports audio-only 37551 * and video-only streams. 37552 */ 37553 _CoalesceStream = function CoalesceStream(options, metadataStream) { 37554 // Number of Tracks per output segment 37555 // If greater than 1, we combine multiple 37556 // tracks into a single segment 37557 this.numberOfTracks = 0; 37558 this.metadataStream = metadataStream; 37559 37560 if (typeof options.remux !== 'undefined') { 37561 this.remuxTracks = !!options.remux; 37562 } else { 37563 this.remuxTracks = true; 37564 } 37565 37566 this.pendingTracks = []; 37567 this.videoTrack = null; 37568 this.pendingBoxes = []; 37569 this.pendingCaptions = []; 37570 this.pendingMetadata = []; 37571 this.pendingBytes = 0; 37572 this.emittedTracks = 0; 37573 37574 _CoalesceStream.prototype.init.call(this); 37575 37576 // Take output from multiple 37577 this.push = function (output) { 37578 // buffer incoming captions until the associated video segment 37579 // finishes 37580 if (output.text) { 37581 return this.pendingCaptions.push(output); 37582 } 37583 // buffer incoming id3 tags until the final flush 37584 if (output.frames) { 37585 return this.pendingMetadata.push(output); 37586 } 37587 37588 // Add this track to the list of pending tracks and store 37589 // important information required for the construction of 37590 // the final segment 37591 this.pendingTracks.push(output.track); 37592 this.pendingBoxes.push(output.boxes); 37593 this.pendingBytes += output.boxes.byteLength; 37594 37595 if (output.track.type === 'video') { 37596 this.videoTrack = output.track; 37597 } 37598 if (output.track.type === 'audio') { 37599 this.audioTrack = output.track; 37600 } 37601 }; 37602 }; 37603 37604 _CoalesceStream.prototype = new stream(); 37605 _CoalesceStream.prototype.flush = function (flushSource) { 37606 var offset = 0, 37607 event = { 37608 captions: [], 37609 captionStreams: {}, 37610 metadata: [], 37611 info: {} 37612 }, 37613 caption, 37614 id3, 37615 initSegment, 37616 timelineStartPts = 0, 37617 i; 37618 37619 if (this.pendingTracks.length < this.numberOfTracks) { 37620 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 37621 // Return because we haven't received a flush from a data-generating 37622 // portion of the segment (meaning that we have only recieved meta-data 37623 // or captions.) 37624 return; 37625 } else if (this.remuxTracks) { 37626 // Return until we have enough tracks from the pipeline to remux (if we 37627 // are remuxing audio and video into a single MP4) 37628 return; 37629 } else if (this.pendingTracks.length === 0) { 37630 // In the case where we receive a flush without any data having been 37631 // received we consider it an emitted track for the purposes of coalescing 37632 // `done` events. 37633 // We do this for the case where there is an audio and video track in the 37634 // segment but no audio data. (seen in several playlists with alternate 37635 // audio tracks and no audio present in the main TS segments.) 37636 this.emittedTracks++; 37637 37638 if (this.emittedTracks >= this.numberOfTracks) { 37639 this.trigger('done'); 37640 this.emittedTracks = 0; 37641 } 37642 return; 37643 } 37644 } 37645 37646 if (this.videoTrack) { 37647 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 37648 VIDEO_PROPERTIES.forEach(function (prop) { 37649 event.info[prop] = this.videoTrack[prop]; 37650 }, this); 37651 } else if (this.audioTrack) { 37652 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 37653 AUDIO_PROPERTIES.forEach(function (prop) { 37654 event.info[prop] = this.audioTrack[prop]; 37655 }, this); 37656 } 37657 37658 if (this.pendingTracks.length === 1) { 37659 event.type = this.pendingTracks[0].type; 37660 } else { 37661 event.type = 'combined'; 37662 } 37663 37664 this.emittedTracks += this.pendingTracks.length; 37665 37666 initSegment = mp4Generator.initSegment(this.pendingTracks); 37667 37668 // Create a new typed array to hold the init segment 37669 event.initSegment = new Uint8Array(initSegment.byteLength); 37670 37671 // Create an init segment containing a moov 37672 // and track definitions 37673 event.initSegment.set(initSegment); 37674 37675 // Create a new typed array to hold the moof+mdats 37676 event.data = new Uint8Array(this.pendingBytes); 37677 37678 // Append each moof+mdat (one per track) together 37679 for (i = 0; i < this.pendingBoxes.length; i++) { 37680 event.data.set(this.pendingBoxes[i], offset); 37681 offset += this.pendingBoxes[i].byteLength; 37682 } 37683 37684 // Translate caption PTS times into second offsets into the 37685 // video timeline for the segment, and add track info 37686 for (i = 0; i < this.pendingCaptions.length; i++) { 37687 caption = this.pendingCaptions[i]; 37688 caption.startTime = caption.startPts - timelineStartPts; 37689 caption.startTime /= 90e3; 37690 caption.endTime = caption.endPts - timelineStartPts; 37691 caption.endTime /= 90e3; 37692 event.captionStreams[caption.stream] = true; 37693 event.captions.push(caption); 37694 } 37695 37696 // Translate ID3 frame PTS times into second offsets into the 37697 // video timeline for the segment 37698 for (i = 0; i < this.pendingMetadata.length; i++) { 37699 id3 = this.pendingMetadata[i]; 37700 id3.cueTime = id3.pts - timelineStartPts; 37701 id3.cueTime /= 90e3; 37702 event.metadata.push(id3); 37703 } 37704 // We add this to every single emitted segment even though we only need 37705 // it for the first 37706 event.metadata.dispatchType = this.metadataStream.dispatchType; 37707 37708 // Reset stream state 37709 this.pendingTracks.length = 0; 37710 this.videoTrack = null; 37711 this.pendingBoxes.length = 0; 37712 this.pendingCaptions.length = 0; 37713 this.pendingBytes = 0; 37714 this.pendingMetadata.length = 0; 37715 37716 // Emit the built segment 37717 this.trigger('data', event); 37718 37719 // Only emit `done` if all tracks have been flushed and emitted 37720 if (this.emittedTracks >= this.numberOfTracks) { 37721 this.trigger('done'); 37722 this.emittedTracks = 0; 37723 } 37724 }; 37725 /** 37726 * A Stream that expects MP2T binary data as input and produces 37727 * corresponding media segments, suitable for use with Media Source 37728 * Extension (MSE) implementations that support the ISO BMFF byte 37729 * stream format, like Chrome. 37730 */ 37731 _Transmuxer = function Transmuxer(options) { 37732 var self = this, 37733 hasFlushed = true, 37734 videoTrack, 37735 audioTrack; 37736 37737 _Transmuxer.prototype.init.call(this); 37738 37739 options = options || {}; 37740 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 37741 this.transmuxPipeline_ = {}; 37742 37743 this.setupAacPipeline = function () { 37744 var pipeline = {}; 37745 this.transmuxPipeline_ = pipeline; 37746 37747 pipeline.type = 'aac'; 37748 pipeline.metadataStream = new m2ts_1.MetadataStream(); 37749 37750 // set up the parsing pipeline 37751 pipeline.aacStream = new aac(); 37752 pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio'); 37753 pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata'); 37754 pipeline.adtsStream = new adts(); 37755 pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream); 37756 pipeline.headOfPipeline = pipeline.aacStream; 37757 37758 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 37759 pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 37760 37761 pipeline.metadataStream.on('timestamp', function (frame) { 37762 pipeline.aacStream.setTimestamp(frame.timeStamp); 37763 }); 37764 37765 pipeline.aacStream.on('data', function (data) { 37766 if (data.type === 'timed-metadata' && !pipeline.audioSegmentStream) { 37767 audioTrack = audioTrack || { 37768 timelineStartInfo: { 37769 baseMediaDecodeTime: self.baseMediaDecodeTime 37770 }, 37771 codec: 'adts', 37772 type: 'audio' 37773 }; 37774 // hook up the audio segment stream to the first track with aac data 37775 pipeline.coalesceStream.numberOfTracks++; 37776 pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options); 37777 // Set up the final part of the audio pipeline 37778 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 37779 } 37780 }); 37781 37782 // Re-emit any data coming from the coalesce stream to the outside world 37783 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 37784 // Let the consumer know we have finished flushing the entire pipeline 37785 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 37786 }; 37787 37788 this.setupTsPipeline = function () { 37789 var pipeline = {}; 37790 this.transmuxPipeline_ = pipeline; 37791 37792 pipeline.type = 'ts'; 37793 pipeline.metadataStream = new m2ts_1.MetadataStream(); 37794 37795 // set up the parsing pipeline 37796 pipeline.packetStream = new m2ts_1.TransportPacketStream(); 37797 pipeline.parseStream = new m2ts_1.TransportParseStream(); 37798 pipeline.elementaryStream = new m2ts_1.ElementaryStream(); 37799 pipeline.videoTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('video'); 37800 pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio'); 37801 pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata'); 37802 pipeline.adtsStream = new adts(); 37803 pipeline.h264Stream = new H264Stream(); 37804 pipeline.captionStream = new m2ts_1.CaptionStream(); 37805 pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream); 37806 pipeline.headOfPipeline = pipeline.packetStream; 37807 37808 // disassemble MPEG2-TS packets into elementary streams 37809 pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream); 37810 37811 // !!THIS ORDER IS IMPORTANT!! 37812 // demux the streams 37813 pipeline.elementaryStream.pipe(pipeline.videoTimestampRolloverStream).pipe(pipeline.h264Stream); 37814 pipeline.elementaryStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 37815 37816 pipeline.elementaryStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 37817 37818 // Hook up CEA-608/708 caption stream 37819 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 37820 37821 pipeline.elementaryStream.on('data', function (data) { 37822 var i; 37823 37824 if (data.type === 'metadata') { 37825 i = data.tracks.length; 37826 37827 // scan the tracks listed in the metadata 37828 while (i--) { 37829 if (!videoTrack && data.tracks[i].type === 'video') { 37830 videoTrack = data.tracks[i]; 37831 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 37832 } else if (!audioTrack && data.tracks[i].type === 'audio') { 37833 audioTrack = data.tracks[i]; 37834 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 37835 } 37836 } 37837 37838 // hook up the video segment stream to the first track with h264 data 37839 if (videoTrack && !pipeline.videoSegmentStream) { 37840 pipeline.coalesceStream.numberOfTracks++; 37841 pipeline.videoSegmentStream = new _VideoSegmentStream(videoTrack, options); 37842 37843 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 37844 // When video emits timelineStartInfo data after a flush, we forward that 37845 // info to the AudioSegmentStream, if it exists, because video timeline 37846 // data takes precedence. 37847 if (audioTrack) { 37848 audioTrack.timelineStartInfo = timelineStartInfo; 37849 // On the first segment we trim AAC frames that exist before the 37850 // very earliest DTS we have seen in video because Chrome will 37851 // interpret any video track with a baseMediaDecodeTime that is 37852 // non-zero as a gap. 37853 pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts); 37854 } 37855 }); 37856 37857 pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo')); 37858 37859 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 37860 if (audioTrack) { 37861 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 37862 } 37863 }); 37864 37865 // Set up the final part of the video pipeline 37866 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 37867 } 37868 37869 if (audioTrack && !pipeline.audioSegmentStream) { 37870 // hook up the audio segment stream to the first track with aac data 37871 pipeline.coalesceStream.numberOfTracks++; 37872 pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options); 37873 37874 // Set up the final part of the audio pipeline 37875 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 37876 } 37877 } 37878 }); 37879 37880 // Re-emit any data coming from the coalesce stream to the outside world 37881 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 37882 // Let the consumer know we have finished flushing the entire pipeline 37883 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 37884 }; 37885 37886 // hook up the segment streams once track metadata is delivered 37887 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 37888 var pipeline = this.transmuxPipeline_; 37889 37890 this.baseMediaDecodeTime = baseMediaDecodeTime; 37891 if (audioTrack) { 37892 audioTrack.timelineStartInfo.dts = undefined; 37893 audioTrack.timelineStartInfo.pts = undefined; 37894 clearDtsInfo(audioTrack); 37895 audioTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime; 37896 if (pipeline.audioTimestampRolloverStream) { 37897 pipeline.audioTimestampRolloverStream.discontinuity(); 37898 } 37899 } 37900 if (videoTrack) { 37901 if (pipeline.videoSegmentStream) { 37902 pipeline.videoSegmentStream.gopCache_ = []; 37903 pipeline.videoTimestampRolloverStream.discontinuity(); 37904 } 37905 videoTrack.timelineStartInfo.dts = undefined; 37906 videoTrack.timelineStartInfo.pts = undefined; 37907 clearDtsInfo(videoTrack); 37908 pipeline.captionStream.reset(); 37909 videoTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime; 37910 } 37911 37912 if (pipeline.timedMetadataTimestampRolloverStream) { 37913 pipeline.timedMetadataTimestampRolloverStream.discontinuity(); 37914 } 37915 }; 37916 37917 this.setAudioAppendStart = function (timestamp) { 37918 if (audioTrack) { 37919 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 37920 } 37921 }; 37922 37923 this.alignGopsWith = function (gopsToAlignWith) { 37924 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 37925 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 37926 } 37927 }; 37928 37929 // feed incoming data to the front of the parsing pipeline 37930 this.push = function (data) { 37931 if (hasFlushed) { 37932 var isAac = isLikelyAacData(data); 37933 37934 if (isAac && this.transmuxPipeline_.type !== 'aac') { 37935 this.setupAacPipeline(); 37936 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 37937 this.setupTsPipeline(); 37938 } 37939 hasFlushed = false; 37940 } 37941 this.transmuxPipeline_.headOfPipeline.push(data); 37942 }; 37943 37944 // flush any buffered data 37945 this.flush = function () { 37946 hasFlushed = true; 37947 // Start at the top of the pipeline and flush all pending work 37948 this.transmuxPipeline_.headOfPipeline.flush(); 37949 }; 37950 37951 // Caption data has to be reset when seeking outside buffered range 37952 this.resetCaptions = function () { 37953 if (this.transmuxPipeline_.captionStream) { 37954 this.transmuxPipeline_.captionStream.reset(); 37955 } 37956 }; 37957 }; 37958 _Transmuxer.prototype = new stream(); 37959 37960 var transmuxer = { 37961 Transmuxer: _Transmuxer, 37962 VideoSegmentStream: _VideoSegmentStream, 37963 AudioSegmentStream: _AudioSegmentStream, 37964 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 37965 VIDEO_PROPERTIES: VIDEO_PROPERTIES 37966 }; 37967 37968 var mp4 = { 37969 generator: mp4Generator, 37970 Transmuxer: transmuxer.Transmuxer, 37971 AudioSegmentStream: transmuxer.AudioSegmentStream, 37972 VideoSegmentStream: transmuxer.VideoSegmentStream 37973 }; 37974 37975 var classCallCheck$$1 = function classCallCheck$$1(instance, Constructor) { 37976 if (!(instance instanceof Constructor)) { 37977 throw new TypeError("Cannot call a class as a function"); 37978 } 37979 }; 37980 37981 var createClass$$1 = function () { 37982 function defineProperties(target, props) { 37983 for (var i = 0; i < props.length; i++) { 37984 var descriptor = props[i]; 37985 descriptor.enumerable = descriptor.enumerable || false; 37986 descriptor.configurable = true; 37987 if ("value" in descriptor) descriptor.writable = true; 37988 Object.defineProperty(target, descriptor.key, descriptor); 37989 } 37990 } 37991 37992 return function (Constructor, protoProps, staticProps) { 37993 if (protoProps) defineProperties(Constructor.prototype, protoProps); 37994 if (staticProps) defineProperties(Constructor, staticProps); 37995 return Constructor; 37996 }; 37997 }(); 37998 37999 /** 38000 * @file transmuxer-worker.js 38001 */ 38002 38003 /** 38004 * Re-emits transmuxer events by converting them into messages to the 38005 * world outside the worker. 38006 * 38007 * @param {Object} transmuxer the transmuxer to wire events on 38008 * @private 38009 */ 38010 var wireTransmuxerEvents = function wireTransmuxerEvents(transmuxer) { 38011 transmuxer.on('data', function (segment) { 38012 // transfer ownership of the underlying ArrayBuffer 38013 // instead of doing a copy to save memory 38014 // ArrayBuffers are transferable but generic TypedArrays are not 38015 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 38016 var initArray = segment.initSegment; 38017 38018 segment.initSegment = { 38019 data: initArray.buffer, 38020 byteOffset: initArray.byteOffset, 38021 byteLength: initArray.byteLength 38022 }; 38023 38024 var typedArray = segment.data; 38025 38026 segment.data = typedArray.buffer; 38027 window_1$$1.postMessage({ 38028 action: 'data', 38029 segment: segment, 38030 byteOffset: typedArray.byteOffset, 38031 byteLength: typedArray.byteLength 38032 }, [segment.data]); 38033 }); 38034 38035 if (transmuxer.captionStream) { 38036 transmuxer.captionStream.on('data', function (caption) { 38037 window_1$$1.postMessage({ 38038 action: 'caption', 38039 data: caption 38040 }); 38041 }); 38042 } 38043 38044 transmuxer.on('done', function (data) { 38045 window_1$$1.postMessage({ action: 'done' }); 38046 }); 38047 38048 transmuxer.on('gopInfo', function (gopInfo) { 38049 window_1$$1.postMessage({ 38050 action: 'gopInfo', 38051 gopInfo: gopInfo 38052 }); 38053 }); 38054 }; 38055 38056 /** 38057 * All incoming messages route through this hash. If no function exists 38058 * to handle an incoming message, then we ignore the message. 38059 * 38060 * @class MessageHandlers 38061 * @param {Object} options the options to initialize with 38062 */ 38063 38064 var MessageHandlers = function () { 38065 function MessageHandlers(options) { 38066 classCallCheck$$1(this, MessageHandlers); 38067 38068 this.options = options || {}; 38069 this.init(); 38070 } 38071 38072 /** 38073 * initialize our web worker and wire all the events. 38074 */ 38075 38076 createClass$$1(MessageHandlers, [{ 38077 key: 'init', 38078 value: function init() { 38079 if (this.transmuxer) { 38080 this.transmuxer.dispose(); 38081 } 38082 this.transmuxer = new mp4.Transmuxer(this.options); 38083 wireTransmuxerEvents(this.transmuxer); 38084 } 38085 38086 /** 38087 * Adds data (a ts segment) to the start of the transmuxer pipeline for 38088 * processing. 38089 * 38090 * @param {ArrayBuffer} data data to push into the muxer 38091 */ 38092 38093 }, { 38094 key: 'push', 38095 value: function push(data) { 38096 // Cast array buffer to correct type for transmuxer 38097 var segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 38098 38099 this.transmuxer.push(segment); 38100 } 38101 38102 /** 38103 * Recreate the transmuxer so that the next segment added via `push` 38104 * start with a fresh transmuxer. 38105 */ 38106 38107 }, { 38108 key: 'reset', 38109 value: function reset() { 38110 this.init(); 38111 } 38112 38113 /** 38114 * Set the value that will be used as the `baseMediaDecodeTime` time for the 38115 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 38116 * set relative to the first based on the PTS values. 38117 * 38118 * @param {Object} data used to set the timestamp offset in the muxer 38119 */ 38120 38121 }, { 38122 key: 'setTimestampOffset', 38123 value: function setTimestampOffset(data) { 38124 var timestampOffset = data.timestampOffset || 0; 38125 38126 this.transmuxer.setBaseMediaDecodeTime(Math.round(timestampOffset * 90000)); 38127 } 38128 }, { 38129 key: 'setAudioAppendStart', 38130 value: function setAudioAppendStart(data) { 38131 this.transmuxer.setAudioAppendStart(Math.ceil(data.appendStart * 90000)); 38132 } 38133 38134 /** 38135 * Forces the pipeline to finish processing the last segment and emit it's 38136 * results. 38137 * 38138 * @param {Object} data event data, not really used 38139 */ 38140 38141 }, { 38142 key: 'flush', 38143 value: function flush(data) { 38144 this.transmuxer.flush(); 38145 } 38146 }, { 38147 key: 'resetCaptions', 38148 value: function resetCaptions() { 38149 this.transmuxer.resetCaptions(); 38150 } 38151 }, { 38152 key: 'alignGopsWith', 38153 value: function alignGopsWith(data) { 38154 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 38155 } 38156 }]); 38157 return MessageHandlers; 38158 }(); 38159 38160 /** 38161 * Our web wroker interface so that things can talk to mux.js 38162 * that will be running in a web worker. the scope is passed to this by 38163 * webworkify. 38164 * 38165 * @param {Object} self the scope for the web worker 38166 */ 38167 38168 var TransmuxerWorker = function TransmuxerWorker(self) { 38169 self.onmessage = function (event) { 38170 if (event.data.action === 'init' && event.data.options) { 38171 this.messageHandlers = new MessageHandlers(event.data.options); 38172 return; 38173 } 38174 38175 if (!this.messageHandlers) { 38176 this.messageHandlers = new MessageHandlers(); 38177 } 38178 38179 if (event.data && event.data.action && event.data.action !== 'init') { 38180 if (this.messageHandlers[event.data.action]) { 38181 this.messageHandlers[event.data.action](event.data); 38182 } 38183 } 38184 }; 38185 }; 38186 38187 var transmuxerWorker = new TransmuxerWorker(self); 38188 38189 return transmuxerWorker; 38190 }(); 38191 }); 38192 38193 /** 38194 * @file codec-utils.js 38195 */ 38196 38197 /** 38198 * Check if a codec string refers to an audio codec. 38199 * 38200 * @param {String} codec codec string to check 38201 * @return {Boolean} if this is an audio codec 38202 * @private 38203 */ 38204 var isAudioCodec = function isAudioCodec(codec) { 38205 return (/mp4a\.\d+.\d+/i.test(codec) 38206 ); 38207 }; 38208 38209 /** 38210 * Check if a codec string refers to a video codec. 38211 * 38212 * @param {String} codec codec string to check 38213 * @return {Boolean} if this is a video codec 38214 * @private 38215 */ 38216 var isVideoCodec = function isVideoCodec(codec) { 38217 return (/avc1\.[\da-f]+/i.test(codec) 38218 ); 38219 }; 38220 38221 /** 38222 * Parse a content type header into a type and parameters 38223 * object 38224 * 38225 * @param {String} type the content type header 38226 * @return {Object} the parsed content-type 38227 * @private 38228 */ 38229 var parseContentType = function parseContentType(type) { 38230 var object = { type: '', parameters: {} }; 38231 var parameters = type.trim().split(';'); 38232 38233 // first parameter should always be content-type 38234 object.type = parameters.shift().trim(); 38235 parameters.forEach(function (parameter) { 38236 var pair = parameter.trim().split('='); 38237 38238 if (pair.length > 1) { 38239 var name = pair[0].replace(/"/g, '').trim(); 38240 var value = pair[1].replace(/"/g, '').trim(); 38241 38242 object.parameters[name] = value; 38243 } 38244 }); 38245 38246 return object; 38247 }; 38248 38249 /** 38250 * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard 38251 * `avc1.<hhhhhh>` 38252 * 38253 * @param {Array} codecs an array of codec strings to fix 38254 * @return {Array} the translated codec array 38255 * @private 38256 */ 38257 var translateLegacyCodecs = function translateLegacyCodecs(codecs) { 38258 return codecs.map(function (codec) { 38259 return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { 38260 var profileHex = ('00' + Number(profile).toString(16)).slice(-2); 38261 var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); 38262 38263 return 'avc1.' + profileHex + '00' + avcLevelHex; 38264 }); 38265 }); 38266 }; 38267 38268 /** 38269 * @file virtual-source-buffer.js 38270 */ 38271 38272 // We create a wrapper around the SourceBuffer so that we can manage the 38273 // state of the `updating` property manually. We have to do this because 38274 // Firefox changes `updating` to false long before triggering `updateend` 38275 // events and that was causing strange problems in videojs-contrib-hls 38276 var makeWrappedSourceBuffer = function makeWrappedSourceBuffer(mediaSource, mimeType) { 38277 var sourceBuffer = mediaSource.addSourceBuffer(mimeType); 38278 var wrapper = Object.create(null); 38279 38280 wrapper.updating = false; 38281 wrapper.realBuffer_ = sourceBuffer; 38282 38283 var _loop = function _loop(key) { 38284 if (typeof sourceBuffer[key] === 'function') { 38285 wrapper[key] = function () { 38286 return sourceBuffer[key].apply(sourceBuffer, arguments); 38287 }; 38288 } else if (typeof wrapper[key] === 'undefined') { 38289 Object.defineProperty(wrapper, key, { 38290 get: function get$$1() { 38291 return sourceBuffer[key]; 38292 }, 38293 set: function set$$1(v) { 38294 return sourceBuffer[key] = v; 38295 } 38296 }); 38297 } 38298 }; 38299 38300 for (var key in sourceBuffer) { 38301 _loop(key); 38302 } 38303 38304 return wrapper; 38305 }; 38306 38307 /** 38308 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 38309 * front of current time. 38310 * 38311 * @param {Array} buffer 38312 * The current buffer of gop information 38313 * @param {Player} player 38314 * The player instance 38315 * @param {Double} mapping 38316 * Offset to map display time to stream presentation time 38317 * @return {Array} 38318 * List of gops considered safe to append over 38319 */ 38320 var gopsSafeToAlignWith = function gopsSafeToAlignWith(buffer, player, mapping) { 38321 if (!player || !buffer.length) { 38322 return []; 38323 } 38324 38325 // pts value for current time + 3 seconds to give a bit more wiggle room 38326 var currentTimePts = Math.ceil((player.currentTime() - mapping + 3) * 90000); 38327 38328 var i = void 0; 38329 38330 for (i = 0; i < buffer.length; i++) { 38331 if (buffer[i].pts > currentTimePts) { 38332 break; 38333 } 38334 } 38335 38336 return buffer.slice(i); 38337 }; 38338 38339 /** 38340 * Appends gop information (timing and byteLength) received by the transmuxer for the 38341 * gops appended in the last call to appendBuffer 38342 * 38343 * @param {Array} buffer 38344 * The current buffer of gop information 38345 * @param {Array} gops 38346 * List of new gop information 38347 * @param {boolean} replace 38348 * If true, replace the buffer with the new gop information. If false, append the 38349 * new gop information to the buffer in the right location of time. 38350 * @return {Array} 38351 * Updated list of gop information 38352 */ 38353 var updateGopBuffer = function updateGopBuffer(buffer, gops, replace) { 38354 if (!gops.length) { 38355 return buffer; 38356 } 38357 38358 if (replace) { 38359 // If we are in safe append mode, then completely overwrite the gop buffer 38360 // with the most recent appeneded data. This will make sure that when appending 38361 // future segments, we only try to align with gops that are both ahead of current 38362 // time and in the last segment appended. 38363 return gops.slice(); 38364 } 38365 38366 var start = gops[0].pts; 38367 38368 var i = 0; 38369 38370 for (i; i < buffer.length; i++) { 38371 if (buffer[i].pts >= start) { 38372 break; 38373 } 38374 } 38375 38376 return buffer.slice(0, i).concat(gops); 38377 }; 38378 38379 /** 38380 * Removes gop information in buffer that overlaps with provided start and end 38381 * 38382 * @param {Array} buffer 38383 * The current buffer of gop information 38384 * @param {Double} start 38385 * position to start the remove at 38386 * @param {Double} end 38387 * position to end the remove at 38388 * @param {Double} mapping 38389 * Offset to map display time to stream presentation time 38390 */ 38391 var removeGopBuffer = function removeGopBuffer(buffer, start, end, mapping) { 38392 var startPts = Math.ceil((start - mapping) * 90000); 38393 var endPts = Math.ceil((end - mapping) * 90000); 38394 var updatedBuffer = buffer.slice(); 38395 38396 var i = buffer.length; 38397 38398 while (i--) { 38399 if (buffer[i].pts <= endPts) { 38400 break; 38401 } 38402 } 38403 38404 if (i === -1) { 38405 // no removal because end of remove range is before start of buffer 38406 return updatedBuffer; 38407 } 38408 38409 var j = i + 1; 38410 38411 while (j--) { 38412 if (buffer[j].pts <= startPts) { 38413 break; 38414 } 38415 } 38416 38417 // clamp remove range start to 0 index 38418 j = Math.max(j, 0); 38419 38420 updatedBuffer.splice(j, i - j + 1); 38421 38422 return updatedBuffer; 38423 }; 38424 38425 /** 38426 * VirtualSourceBuffers exist so that we can transmux non native formats 38427 * into a native format, but keep the same api as a native source buffer. 38428 * It creates a transmuxer, that works in its own thread (a web worker) and 38429 * that transmuxer muxes the data into a native format. VirtualSourceBuffer will 38430 * then send all of that data to the naive sourcebuffer so that it is 38431 * indestinguishable from a natively supported format. 38432 * 38433 * @param {HtmlMediaSource} mediaSource the parent mediaSource 38434 * @param {Array} codecs array of codecs that we will be dealing with 38435 * @class VirtualSourceBuffer 38436 * @extends video.js.EventTarget 38437 */ 38438 38439 var VirtualSourceBuffer = function (_videojs$EventTarget) { 38440 inherits$3(VirtualSourceBuffer, _videojs$EventTarget); 38441 38442 function VirtualSourceBuffer(mediaSource, codecs) { 38443 classCallCheck$3(this, VirtualSourceBuffer); 38444 38445 var _this = possibleConstructorReturn$3(this, (VirtualSourceBuffer.__proto__ || Object.getPrototypeOf(VirtualSourceBuffer)).call(this, videojs$1.EventTarget)); 38446 38447 _this.timestampOffset_ = 0; 38448 _this.pendingBuffers_ = []; 38449 _this.bufferUpdating_ = false; 38450 38451 _this.mediaSource_ = mediaSource; 38452 _this.codecs_ = codecs; 38453 _this.audioCodec_ = null; 38454 _this.videoCodec_ = null; 38455 _this.audioDisabled_ = false; 38456 _this.appendAudioInitSegment_ = true; 38457 _this.gopBuffer_ = []; 38458 _this.timeMapping_ = 0; 38459 _this.safeAppend_ = videojs$1.browser.IE_VERSION >= 11; 38460 38461 var options = { 38462 remux: false, 38463 alignGopsAtEnd: _this.safeAppend_ 38464 }; 38465 38466 _this.codecs_.forEach(function (codec) { 38467 if (isAudioCodec(codec)) { 38468 _this.audioCodec_ = codec; 38469 } else if (isVideoCodec(codec)) { 38470 _this.videoCodec_ = codec; 38471 } 38472 }); 38473 38474 // append muxed segments to their respective native buffers as 38475 // soon as they are available 38476 _this.transmuxer_ = new TransmuxWorker(); 38477 _this.transmuxer_.postMessage({ action: 'init', options: options }); 38478 38479 _this.transmuxer_.onmessage = function (event) { 38480 if (event.data.action === 'data') { 38481 return _this.data_(event); 38482 } 38483 38484 if (event.data.action === 'done') { 38485 return _this.done_(event); 38486 } 38487 38488 if (event.data.action === 'gopInfo') { 38489 return _this.appendGopInfo_(event); 38490 } 38491 }; 38492 38493 // this timestampOffset is a property with the side-effect of resetting 38494 // baseMediaDecodeTime in the transmuxer on the setter 38495 Object.defineProperty(_this, 'timestampOffset', { 38496 get: function get$$1() { 38497 return this.timestampOffset_; 38498 }, 38499 set: function set$$1(val) { 38500 if (typeof val === 'number' && val >= 0) { 38501 this.timestampOffset_ = val; 38502 this.appendAudioInitSegment_ = true; 38503 38504 // reset gop buffer on timestampoffset as this signals a change in timeline 38505 this.gopBuffer_.length = 0; 38506 this.timeMapping_ = 0; 38507 38508 // We have to tell the transmuxer to set the baseMediaDecodeTime to 38509 // the desired timestampOffset for the next segment 38510 this.transmuxer_.postMessage({ 38511 action: 'setTimestampOffset', 38512 timestampOffset: val 38513 }); 38514 } 38515 } 38516 }); 38517 38518 // setting the append window affects both source buffers 38519 Object.defineProperty(_this, 'appendWindowStart', { 38520 get: function get$$1() { 38521 return (this.videoBuffer_ || this.audioBuffer_).appendWindowStart; 38522 }, 38523 set: function set$$1(start) { 38524 if (this.videoBuffer_) { 38525 this.videoBuffer_.appendWindowStart = start; 38526 } 38527 if (this.audioBuffer_) { 38528 this.audioBuffer_.appendWindowStart = start; 38529 } 38530 } 38531 }); 38532 38533 // this buffer is "updating" if either of its native buffers are 38534 Object.defineProperty(_this, 'updating', { 38535 get: function get$$1() { 38536 return !!(this.bufferUpdating_ || !this.audioDisabled_ && this.audioBuffer_ && this.audioBuffer_.updating || this.videoBuffer_ && this.videoBuffer_.updating); 38537 } 38538 }); 38539 38540 // the buffered property is the intersection of the buffered 38541 // ranges of the native source buffers 38542 Object.defineProperty(_this, 'buffered', { 38543 get: function get$$1() { 38544 var start = null; 38545 var end = null; 38546 var arity = 0; 38547 var extents = []; 38548 var ranges = []; 38549 38550 // neither buffer has been created yet 38551 if (!this.videoBuffer_ && !this.audioBuffer_) { 38552 return videojs$1.createTimeRange(); 38553 } 38554 38555 // only one buffer is configured 38556 if (!this.videoBuffer_) { 38557 return this.audioBuffer_.buffered; 38558 } 38559 if (!this.audioBuffer_) { 38560 return this.videoBuffer_.buffered; 38561 } 38562 38563 // both buffers are configured 38564 if (this.audioDisabled_) { 38565 return this.videoBuffer_.buffered; 38566 } 38567 38568 // both buffers are empty 38569 if (this.videoBuffer_.buffered.length === 0 && this.audioBuffer_.buffered.length === 0) { 38570 return videojs$1.createTimeRange(); 38571 } 38572 38573 // Handle the case where we have both buffers and create an 38574 // intersection of the two 38575 var videoBuffered = this.videoBuffer_.buffered; 38576 var audioBuffered = this.audioBuffer_.buffered; 38577 var count = videoBuffered.length; 38578 38579 // A) Gather up all start and end times 38580 while (count--) { 38581 extents.push({ time: videoBuffered.start(count), type: 'start' }); 38582 extents.push({ time: videoBuffered.end(count), type: 'end' }); 38583 } 38584 count = audioBuffered.length; 38585 while (count--) { 38586 extents.push({ time: audioBuffered.start(count), type: 'start' }); 38587 extents.push({ time: audioBuffered.end(count), type: 'end' }); 38588 } 38589 // B) Sort them by time 38590 extents.sort(function (a, b) { 38591 return a.time - b.time; 38592 }); 38593 38594 // C) Go along one by one incrementing arity for start and decrementing 38595 // arity for ends 38596 for (count = 0; count < extents.length; count++) { 38597 if (extents[count].type === 'start') { 38598 arity++; 38599 38600 // D) If arity is ever incremented to 2 we are entering an 38601 // overlapping range 38602 if (arity === 2) { 38603 start = extents[count].time; 38604 } 38605 } else if (extents[count].type === 'end') { 38606 arity--; 38607 38608 // E) If arity is ever decremented to 1 we leaving an 38609 // overlapping range 38610 if (arity === 1) { 38611 end = extents[count].time; 38612 } 38613 } 38614 38615 // F) Record overlapping ranges 38616 if (start !== null && end !== null) { 38617 ranges.push([start, end]); 38618 start = null; 38619 end = null; 38620 } 38621 } 38622 38623 return videojs$1.createTimeRanges(ranges); 38624 } 38625 }); 38626 return _this; 38627 } 38628 38629 /** 38630 * When we get a data event from the transmuxer 38631 * we call this function and handle the data that 38632 * was sent to us 38633 * 38634 * @private 38635 * @param {Event} event the data event from the transmuxer 38636 */ 38637 38638 createClass$2(VirtualSourceBuffer, [{ 38639 key: 'data_', 38640 value: function data_(event) { 38641 var segment = event.data.segment; 38642 38643 // Cast ArrayBuffer to TypedArray 38644 segment.data = new Uint8Array(segment.data, event.data.byteOffset, event.data.byteLength); 38645 38646 segment.initSegment = new Uint8Array(segment.initSegment.data, segment.initSegment.byteOffset, segment.initSegment.byteLength); 38647 38648 createTextTracksIfNecessary(this, this.mediaSource_, segment); 38649 38650 // Add the segments to the pendingBuffers array 38651 this.pendingBuffers_.push(segment); 38652 return; 38653 } 38654 38655 /** 38656 * When we get a done event from the transmuxer 38657 * we call this function and we process all 38658 * of the pending data that we have been saving in the 38659 * data_ function 38660 * 38661 * @private 38662 * @param {Event} event the done event from the transmuxer 38663 */ 38664 38665 }, { 38666 key: 'done_', 38667 value: function done_(event) { 38668 // Don't process and append data if the mediaSource is closed 38669 if (this.mediaSource_.readyState === 'closed') { 38670 this.pendingBuffers_.length = 0; 38671 return; 38672 } 38673 38674 // All buffers should have been flushed from the muxer 38675 // start processing anything we have received 38676 this.processPendingSegments_(); 38677 return; 38678 } 38679 38680 /** 38681 * Create our internal native audio/video source buffers and add 38682 * event handlers to them with the following conditions: 38683 * 1. they do not already exist on the mediaSource 38684 * 2. this VSB has a codec for them 38685 * 38686 * @private 38687 */ 38688 38689 }, { 38690 key: 'createRealSourceBuffers_', 38691 value: function createRealSourceBuffers_() { 38692 var _this2 = this; 38693 38694 var types = ['audio', 'video']; 38695 38696 types.forEach(function (type) { 38697 // Don't create a SourceBuffer of this type if we don't have a 38698 // codec for it 38699 if (!_this2[type + 'Codec_']) { 38700 return; 38701 } 38702 38703 // Do nothing if a SourceBuffer of this type already exists 38704 if (_this2[type + 'Buffer_']) { 38705 return; 38706 } 38707 38708 var buffer = null; 38709 38710 // If the mediasource already has a SourceBuffer for the codec 38711 // use that 38712 if (_this2.mediaSource_[type + 'Buffer_']) { 38713 buffer = _this2.mediaSource_[type + 'Buffer_']; 38714 // In multiple audio track cases, the audio source buffer is disabled 38715 // on the main VirtualSourceBuffer by the HTMLMediaSource much earlier 38716 // than createRealSourceBuffers_ is called to create the second 38717 // VirtualSourceBuffer because that happens as a side-effect of 38718 // videojs-contrib-hls starting the audioSegmentLoader. As a result, 38719 // the audioBuffer is essentially "ownerless" and no one will toggle 38720 // the `updating` state back to false once the `updateend` event is received 38721 // 38722 // Setting `updating` to false manually will work around this 38723 // situation and allow work to continue 38724 buffer.updating = false; 38725 } else { 38726 var codecProperty = type + 'Codec_'; 38727 var mimeType = type + '/mp4;codecs="' + _this2[codecProperty] + '"'; 38728 38729 buffer = makeWrappedSourceBuffer(_this2.mediaSource_.nativeMediaSource_, mimeType); 38730 38731 _this2.mediaSource_[type + 'Buffer_'] = buffer; 38732 } 38733 38734 _this2[type + 'Buffer_'] = buffer; 38735 38736 // Wire up the events to the SourceBuffer 38737 ['update', 'updatestart', 'updateend'].forEach(function (event) { 38738 buffer.addEventListener(event, function () { 38739 // if audio is disabled 38740 if (type === 'audio' && _this2.audioDisabled_) { 38741 return; 38742 } 38743 38744 if (event === 'updateend') { 38745 _this2[type + 'Buffer_'].updating = false; 38746 } 38747 38748 var shouldTrigger = types.every(function (t) { 38749 // skip checking audio's updating status if audio 38750 // is not enabled 38751 if (t === 'audio' && _this2.audioDisabled_) { 38752 return true; 38753 } 38754 // if the other type if updating we don't trigger 38755 if (type !== t && _this2[t + 'Buffer_'] && _this2[t + 'Buffer_'].updating) { 38756 return false; 38757 } 38758 return true; 38759 }); 38760 38761 if (shouldTrigger) { 38762 return _this2.trigger(event); 38763 } 38764 }); 38765 }); 38766 }); 38767 } 38768 38769 /** 38770 * Emulate the native mediasource function, but our function will 38771 * send all of the proposed segments to the transmuxer so that we 38772 * can transmux them before we append them to our internal 38773 * native source buffers in the correct format. 38774 * 38775 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer 38776 * @param {Uint8Array} segment the segment to append to the buffer 38777 */ 38778 38779 }, { 38780 key: 'appendBuffer', 38781 value: function appendBuffer(segment) { 38782 // Start the internal "updating" state 38783 this.bufferUpdating_ = true; 38784 38785 if (this.audioBuffer_ && this.audioBuffer_.buffered.length) { 38786 var audioBuffered = this.audioBuffer_.buffered; 38787 38788 this.transmuxer_.postMessage({ 38789 action: 'setAudioAppendStart', 38790 appendStart: audioBuffered.end(audioBuffered.length - 1) 38791 }); 38792 } 38793 38794 if (this.videoBuffer_) { 38795 this.transmuxer_.postMessage({ 38796 action: 'alignGopsWith', 38797 gopsToAlignWith: gopsSafeToAlignWith(this.gopBuffer_, this.mediaSource_.player_, this.timeMapping_) 38798 }); 38799 } 38800 38801 this.transmuxer_.postMessage({ 38802 action: 'push', 38803 // Send the typed-array of data as an ArrayBuffer so that 38804 // it can be sent as a "Transferable" and avoid the costly 38805 // memory copy 38806 data: segment.buffer, 38807 38808 // To recreate the original typed-array, we need information 38809 // about what portion of the ArrayBuffer it was a view into 38810 byteOffset: segment.byteOffset, 38811 byteLength: segment.byteLength 38812 }, [segment.buffer]); 38813 this.transmuxer_.postMessage({ action: 'flush' }); 38814 } 38815 38816 /** 38817 * Appends gop information (timing and byteLength) received by the transmuxer for the 38818 * gops appended in the last call to appendBuffer 38819 * 38820 * @param {Event} event 38821 * The gopInfo event from the transmuxer 38822 * @param {Array} event.data.gopInfo 38823 * List of gop info to append 38824 */ 38825 38826 }, { 38827 key: 'appendGopInfo_', 38828 value: function appendGopInfo_(event) { 38829 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, event.data.gopInfo, this.safeAppend_); 38830 } 38831 38832 /** 38833 * Emulate the native mediasource function and remove parts 38834 * of the buffer from any of our internal buffers that exist 38835 * 38836 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/remove 38837 * @param {Double} start position to start the remove at 38838 * @param {Double} end position to end the remove at 38839 */ 38840 38841 }, { 38842 key: 'remove', 38843 value: function remove(start, end) { 38844 if (this.videoBuffer_) { 38845 this.videoBuffer_.updating = true; 38846 this.videoBuffer_.remove(start, end); 38847 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 38848 } 38849 if (!this.audioDisabled_ && this.audioBuffer_) { 38850 this.audioBuffer_.updating = true; 38851 this.audioBuffer_.remove(start, end); 38852 } 38853 38854 // Remove Metadata Cues (id3) 38855 removeCuesFromTrack(start, end, this.metadataTrack_); 38856 38857 // Remove Any Captions 38858 if (this.inbandTextTracks_) { 38859 for (var track in this.inbandTextTracks_) { 38860 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 38861 } 38862 } 38863 } 38864 38865 /** 38866 * Process any segments that the muxer has output 38867 * Concatenate segments together based on type and append them into 38868 * their respective sourceBuffers 38869 * 38870 * @private 38871 */ 38872 38873 }, { 38874 key: 'processPendingSegments_', 38875 value: function processPendingSegments_() { 38876 var sortedSegments = { 38877 video: { 38878 segments: [], 38879 bytes: 0 38880 }, 38881 audio: { 38882 segments: [], 38883 bytes: 0 38884 }, 38885 captions: [], 38886 metadata: [] 38887 }; 38888 38889 // Sort segments into separate video/audio arrays and 38890 // keep track of their total byte lengths 38891 sortedSegments = this.pendingBuffers_.reduce(function (segmentObj, segment) { 38892 var type = segment.type; 38893 var data = segment.data; 38894 var initSegment = segment.initSegment; 38895 38896 segmentObj[type].segments.push(data); 38897 segmentObj[type].bytes += data.byteLength; 38898 38899 segmentObj[type].initSegment = initSegment; 38900 38901 // Gather any captions into a single array 38902 if (segment.captions) { 38903 segmentObj.captions = segmentObj.captions.concat(segment.captions); 38904 } 38905 38906 if (segment.info) { 38907 segmentObj[type].info = segment.info; 38908 } 38909 38910 // Gather any metadata into a single array 38911 if (segment.metadata) { 38912 segmentObj.metadata = segmentObj.metadata.concat(segment.metadata); 38913 } 38914 38915 return segmentObj; 38916 }, sortedSegments); 38917 38918 // Create the real source buffers if they don't exist by now since we 38919 // finally are sure what tracks are contained in the source 38920 if (!this.videoBuffer_ && !this.audioBuffer_) { 38921 // Remove any codecs that may have been specified by default but 38922 // are no longer applicable now 38923 if (sortedSegments.video.bytes === 0) { 38924 this.videoCodec_ = null; 38925 } 38926 if (sortedSegments.audio.bytes === 0) { 38927 this.audioCodec_ = null; 38928 } 38929 38930 this.createRealSourceBuffers_(); 38931 } 38932 38933 if (sortedSegments.audio.info) { 38934 this.mediaSource_.trigger({ type: 'audioinfo', info: sortedSegments.audio.info }); 38935 } 38936 if (sortedSegments.video.info) { 38937 this.mediaSource_.trigger({ type: 'videoinfo', info: sortedSegments.video.info }); 38938 } 38939 38940 if (this.appendAudioInitSegment_) { 38941 if (!this.audioDisabled_ && this.audioBuffer_) { 38942 sortedSegments.audio.segments.unshift(sortedSegments.audio.initSegment); 38943 sortedSegments.audio.bytes += sortedSegments.audio.initSegment.byteLength; 38944 } 38945 this.appendAudioInitSegment_ = false; 38946 } 38947 38948 var triggerUpdateend = false; 38949 38950 // Merge multiple video and audio segments into one and append 38951 if (this.videoBuffer_ && sortedSegments.video.bytes) { 38952 sortedSegments.video.segments.unshift(sortedSegments.video.initSegment); 38953 sortedSegments.video.bytes += sortedSegments.video.initSegment.byteLength; 38954 this.concatAndAppendSegments_(sortedSegments.video, this.videoBuffer_); 38955 // TODO: are video tracks the only ones with text tracks? 38956 addTextTrackData(this, sortedSegments.captions, sortedSegments.metadata); 38957 } else if (this.videoBuffer_ && (this.audioDisabled_ || !this.audioBuffer_)) { 38958 // The transmuxer did not return any bytes of video, meaning it was all trimmed 38959 // for gop alignment. Since we have a video buffer and audio is disabled, updateend 38960 // will never be triggered by this source buffer, which will cause contrib-hls 38961 // to be stuck forever waiting for updateend. If audio is not disabled, updateend 38962 // will be triggered by the audio buffer, which will be sent upwards since the video 38963 // buffer will not be in an updating state. 38964 triggerUpdateend = true; 38965 } 38966 38967 if (!this.audioDisabled_ && this.audioBuffer_) { 38968 this.concatAndAppendSegments_(sortedSegments.audio, this.audioBuffer_); 38969 } 38970 38971 this.pendingBuffers_.length = 0; 38972 38973 if (triggerUpdateend) { 38974 this.trigger('updateend'); 38975 } 38976 38977 // We are no longer in the internal "updating" state 38978 this.bufferUpdating_ = false; 38979 } 38980 38981 /** 38982 * Combine all segments into a single Uint8Array and then append them 38983 * to the destination buffer 38984 * 38985 * @param {Object} segmentObj 38986 * @param {SourceBuffer} destinationBuffer native source buffer to append data to 38987 * @private 38988 */ 38989 38990 }, { 38991 key: 'concatAndAppendSegments_', 38992 value: function concatAndAppendSegments_(segmentObj, destinationBuffer) { 38993 var offset = 0; 38994 var tempBuffer = void 0; 38995 38996 if (segmentObj.bytes) { 38997 tempBuffer = new Uint8Array(segmentObj.bytes); 38998 38999 // Combine the individual segments into one large typed-array 39000 segmentObj.segments.forEach(function (segment) { 39001 tempBuffer.set(segment, offset); 39002 offset += segment.byteLength; 39003 }); 39004 39005 try { 39006 destinationBuffer.updating = true; 39007 destinationBuffer.appendBuffer(tempBuffer); 39008 } catch (error) { 39009 if (this.mediaSource_.player_) { 39010 this.mediaSource_.player_.error({ 39011 code: -3, 39012 type: 'APPEND_BUFFER_ERR', 39013 message: error.message, 39014 originalError: error 39015 }); 39016 } 39017 } 39018 } 39019 } 39020 39021 /** 39022 * Emulate the native mediasource function. abort any soureBuffer 39023 * actions and throw out any un-appended data. 39024 * 39025 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/abort 39026 */ 39027 39028 }, { 39029 key: 'abort', 39030 value: function abort() { 39031 if (this.videoBuffer_) { 39032 this.videoBuffer_.abort(); 39033 } 39034 if (!this.audioDisabled_ && this.audioBuffer_) { 39035 this.audioBuffer_.abort(); 39036 } 39037 if (this.transmuxer_) { 39038 this.transmuxer_.postMessage({ action: 'reset' }); 39039 } 39040 this.pendingBuffers_.length = 0; 39041 this.bufferUpdating_ = false; 39042 } 39043 }]); 39044 return VirtualSourceBuffer; 39045 }(videojs$1.EventTarget); 39046 39047 /** 39048 * @file html-media-source.js 39049 */ 39050 39051 /** 39052 * Our MediaSource implementation in HTML, mimics native 39053 * MediaSource where/if possible. 39054 * 39055 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource 39056 * @class HtmlMediaSource 39057 * @extends videojs.EventTarget 39058 */ 39059 39060 var HtmlMediaSource = function (_videojs$EventTarget) { 39061 inherits$3(HtmlMediaSource, _videojs$EventTarget); 39062 39063 function HtmlMediaSource() { 39064 classCallCheck$3(this, HtmlMediaSource); 39065 39066 var _this = possibleConstructorReturn$3(this, (HtmlMediaSource.__proto__ || Object.getPrototypeOf(HtmlMediaSource)).call(this)); 39067 39068 var property = void 0; 39069 39070 _this.nativeMediaSource_ = new window_1.MediaSource(); 39071 // delegate to the native MediaSource's methods by default 39072 for (property in _this.nativeMediaSource_) { 39073 if (!(property in HtmlMediaSource.prototype) && typeof _this.nativeMediaSource_[property] === 'function') { 39074 _this[property] = _this.nativeMediaSource_[property].bind(_this.nativeMediaSource_); 39075 } 39076 } 39077 39078 // emulate `duration` and `seekable` until seeking can be 39079 // handled uniformly for live streams 39080 // see https://github.com/w3c/media-source/issues/5 39081 _this.duration_ = NaN; 39082 Object.defineProperty(_this, 'duration', { 39083 get: function get$$1() { 39084 if (this.duration_ === Infinity) { 39085 return this.duration_; 39086 } 39087 return this.nativeMediaSource_.duration; 39088 }, 39089 set: function set$$1(duration) { 39090 this.duration_ = duration; 39091 if (duration !== Infinity) { 39092 this.nativeMediaSource_.duration = duration; 39093 return; 39094 } 39095 } 39096 }); 39097 Object.defineProperty(_this, 'seekable', { 39098 get: function get$$1() { 39099 if (this.duration_ === Infinity) { 39100 return videojs$1.createTimeRanges([[0, this.nativeMediaSource_.duration]]); 39101 } 39102 return this.nativeMediaSource_.seekable; 39103 } 39104 }); 39105 39106 Object.defineProperty(_this, 'readyState', { 39107 get: function get$$1() { 39108 return this.nativeMediaSource_.readyState; 39109 } 39110 }); 39111 39112 Object.defineProperty(_this, 'activeSourceBuffers', { 39113 get: function get$$1() { 39114 return this.activeSourceBuffers_; 39115 } 39116 }); 39117 39118 // the list of virtual and native SourceBuffers created by this 39119 // MediaSource 39120 _this.sourceBuffers = []; 39121 39122 _this.activeSourceBuffers_ = []; 39123 39124 /** 39125 * update the list of active source buffers based upon various 39126 * imformation from HLS and video.js 39127 * 39128 * @private 39129 */ 39130 _this.updateActiveSourceBuffers_ = function () { 39131 // Retain the reference but empty the array 39132 _this.activeSourceBuffers_.length = 0; 39133 39134 // If there is only one source buffer, then it will always be active and audio will 39135 // be disabled based on the codec of the source buffer 39136 if (_this.sourceBuffers.length === 1) { 39137 var sourceBuffer = _this.sourceBuffers[0]; 39138 39139 sourceBuffer.appendAudioInitSegment_ = true; 39140 sourceBuffer.audioDisabled_ = !sourceBuffer.audioCodec_; 39141 _this.activeSourceBuffers_.push(sourceBuffer); 39142 return; 39143 } 39144 39145 // There are 2 source buffers, a combined (possibly video only) source buffer and 39146 // and an audio only source buffer. 39147 // By default, the audio in the combined virtual source buffer is enabled 39148 // and the audio-only source buffer (if it exists) is disabled. 39149 var disableCombined = false; 39150 var disableAudioOnly = true; 39151 39152 // TODO: maybe we can store the sourcebuffers on the track objects? 39153 // safari may do something like this 39154 for (var i = 0; i < _this.player_.audioTracks().length; i++) { 39155 var track = _this.player_.audioTracks()[i]; 39156 39157 if (track.enabled && track.kind !== 'main') { 39158 // The enabled track is an alternate audio track so disable the audio in 39159 // the combined source buffer and enable the audio-only source buffer. 39160 disableCombined = true; 39161 disableAudioOnly = false; 39162 break; 39163 } 39164 } 39165 39166 _this.sourceBuffers.forEach(function (sourceBuffer, index) { 39167 /* eslinst-disable */ 39168 // TODO once codecs are required, we can switch to using the codecs to determine 39169 // what stream is the video stream, rather than relying on videoTracks 39170 /* eslinst-enable */ 39171 39172 sourceBuffer.appendAudioInitSegment_ = true; 39173 39174 if (sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) { 39175 // combined 39176 sourceBuffer.audioDisabled_ = disableCombined; 39177 } else if (sourceBuffer.videoCodec_ && !sourceBuffer.audioCodec_) { 39178 // If the "combined" source buffer is video only, then we do not want 39179 // disable the audio-only source buffer (this is mostly for demuxed 39180 // audio and video hls) 39181 sourceBuffer.audioDisabled_ = true; 39182 disableAudioOnly = false; 39183 } else if (!sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) { 39184 // audio only 39185 // In the case of audio only with alternate audio and disableAudioOnly is true 39186 // this means we want to disable the audio on the alternate audio sourcebuffer 39187 // but not the main "combined" source buffer. The "combined" source buffer is 39188 // always at index 0, so this ensures audio won't be disabled in both source 39189 // buffers. 39190 sourceBuffer.audioDisabled_ = index ? disableAudioOnly : !disableAudioOnly; 39191 if (sourceBuffer.audioDisabled_) { 39192 return; 39193 } 39194 } 39195 39196 _this.activeSourceBuffers_.push(sourceBuffer); 39197 }); 39198 }; 39199 39200 _this.onPlayerMediachange_ = function () { 39201 _this.sourceBuffers.forEach(function (sourceBuffer) { 39202 sourceBuffer.appendAudioInitSegment_ = true; 39203 }); 39204 }; 39205 39206 _this.onHlsReset_ = function () { 39207 _this.sourceBuffers.forEach(function (sourceBuffer) { 39208 if (sourceBuffer.transmuxer_) { 39209 sourceBuffer.transmuxer_.postMessage({ action: 'resetCaptions' }); 39210 } 39211 }); 39212 }; 39213 39214 _this.onHlsSegmentTimeMapping_ = function (event) { 39215 _this.sourceBuffers.forEach(function (buffer) { 39216 return buffer.timeMapping_ = event.mapping; 39217 }); 39218 }; 39219 39220 // Re-emit MediaSource events on the polyfill 39221 ['sourceopen', 'sourceclose', 'sourceended'].forEach(function (eventName) { 39222 this.nativeMediaSource_.addEventListener(eventName, this.trigger.bind(this)); 39223 }, _this); 39224 39225 // capture the associated player when the MediaSource is 39226 // successfully attached 39227 _this.on('sourceopen', function (event) { 39228 // Get the player this MediaSource is attached to 39229 var video = document_1.querySelector('[src="' + _this.url_ + '"]'); 39230 39231 if (!video) { 39232 return; 39233 } 39234 39235 _this.player_ = videojs$1(video.parentNode); 39236 39237 // hls-reset is fired by videojs.Hls on to the tech after the main SegmentLoader 39238 // resets its state and flushes the buffer 39239 _this.player_.tech_.on('hls-reset', _this.onHlsReset_); 39240 // hls-segment-time-mapping is fired by videojs.Hls on to the tech after the main 39241 // SegmentLoader inspects an MTS segment and has an accurate stream to display 39242 // time mapping 39243 _this.player_.tech_.on('hls-segment-time-mapping', _this.onHlsSegmentTimeMapping_); 39244 39245 if (_this.player_.audioTracks && _this.player_.audioTracks()) { 39246 _this.player_.audioTracks().on('change', _this.updateActiveSourceBuffers_); 39247 _this.player_.audioTracks().on('addtrack', _this.updateActiveSourceBuffers_); 39248 _this.player_.audioTracks().on('removetrack', _this.updateActiveSourceBuffers_); 39249 } 39250 39251 _this.player_.on('mediachange', _this.onPlayerMediachange_); 39252 }); 39253 39254 _this.on('sourceended', function (event) { 39255 var duration = durationOfVideo(_this.duration); 39256 39257 for (var i = 0; i < _this.sourceBuffers.length; i++) { 39258 var sourcebuffer = _this.sourceBuffers[i]; 39259 var cues = sourcebuffer.metadataTrack_ && sourcebuffer.metadataTrack_.cues; 39260 39261 if (cues && cues.length) { 39262 cues[cues.length - 1].endTime = duration; 39263 } 39264 } 39265 }); 39266 39267 // explicitly terminate any WebWorkers that were created 39268 // by SourceHandlers 39269 _this.on('sourceclose', function (event) { 39270 this.sourceBuffers.forEach(function (sourceBuffer) { 39271 if (sourceBuffer.transmuxer_) { 39272 sourceBuffer.transmuxer_.terminate(); 39273 } 39274 }); 39275 39276 this.sourceBuffers.length = 0; 39277 if (!this.player_) { 39278 return; 39279 } 39280 39281 if (this.player_.audioTracks && this.player_.audioTracks()) { 39282 this.player_.audioTracks().off('change', this.updateActiveSourceBuffers_); 39283 this.player_.audioTracks().off('addtrack', this.updateActiveSourceBuffers_); 39284 this.player_.audioTracks().off('removetrack', this.updateActiveSourceBuffers_); 39285 } 39286 39287 // We can only change this if the player hasn't been disposed of yet 39288 // because `off` eventually tries to use the el_ property. If it has 39289 // been disposed of, then don't worry about it because there are no 39290 // event handlers left to unbind anyway 39291 if (this.player_.el_) { 39292 this.player_.off('mediachange', this.onPlayerMediachange_); 39293 this.player_.tech_.off('hls-reset', this.onHlsReset_); 39294 this.player_.tech_.off('hls-segment-time-mapping', this.onHlsSegmentTimeMapping_); 39295 } 39296 }); 39297 return _this; 39298 } 39299 39300 /** 39301 * Add a range that that can now be seeked to. 39302 * 39303 * @param {Double} start where to start the addition 39304 * @param {Double} end where to end the addition 39305 * @private 39306 */ 39307 39308 createClass$2(HtmlMediaSource, [{ 39309 key: 'addSeekableRange_', 39310 value: function addSeekableRange_(start, end) { 39311 var error = void 0; 39312 39313 if (this.duration !== Infinity) { 39314 error = new Error('MediaSource.addSeekableRange() can only be invoked ' + 'when the duration is Infinity'); 39315 error.name = 'InvalidStateError'; 39316 error.code = 11; 39317 throw error; 39318 } 39319 39320 if (end > this.nativeMediaSource_.duration || isNaN(this.nativeMediaSource_.duration)) { 39321 this.nativeMediaSource_.duration = end; 39322 } 39323 } 39324 39325 /** 39326 * Add a source buffer to the media source. 39327 * 39328 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer 39329 * @param {String} type the content-type of the content 39330 * @return {Object} the created source buffer 39331 */ 39332 39333 }, { 39334 key: 'addSourceBuffer', 39335 value: function addSourceBuffer(type) { 39336 var buffer = void 0; 39337 var parsedType = parseContentType(type); 39338 39339 // Create a VirtualSourceBuffer to transmux MPEG-2 transport 39340 // stream segments into fragmented MP4s 39341 if (/^(video|audio)\/mp2t$/i.test(parsedType.type)) { 39342 var codecs = []; 39343 39344 if (parsedType.parameters && parsedType.parameters.codecs) { 39345 codecs = parsedType.parameters.codecs.split(','); 39346 codecs = translateLegacyCodecs(codecs); 39347 codecs = codecs.filter(function (codec) { 39348 return isAudioCodec(codec) || isVideoCodec(codec); 39349 }); 39350 } 39351 39352 if (codecs.length === 0) { 39353 codecs = ['avc1.4d400d', 'mp4a.40.2']; 39354 } 39355 39356 buffer = new VirtualSourceBuffer(this, codecs); 39357 39358 if (this.sourceBuffers.length !== 0) { 39359 // If another VirtualSourceBuffer already exists, then we are creating a 39360 // SourceBuffer for an alternate audio track and therefore we know that 39361 // the source has both an audio and video track. 39362 // That means we should trigger the manual creation of the real 39363 // SourceBuffers instead of waiting for the transmuxer to return data 39364 this.sourceBuffers[0].createRealSourceBuffers_(); 39365 buffer.createRealSourceBuffers_(); 39366 39367 // Automatically disable the audio on the first source buffer if 39368 // a second source buffer is ever created 39369 this.sourceBuffers[0].audioDisabled_ = true; 39370 } 39371 } else { 39372 // delegate to the native implementation 39373 buffer = this.nativeMediaSource_.addSourceBuffer(type); 39374 } 39375 39376 this.sourceBuffers.push(buffer); 39377 return buffer; 39378 } 39379 }]); 39380 return HtmlMediaSource; 39381 }(videojs$1.EventTarget); 39382 39383 /** 39384 * @file videojs-contrib-media-sources.js 39385 */ 39386 var urlCount = 0; 39387 39388 // ------------ 39389 // Media Source 39390 // ------------ 39391 39392 // store references to the media sources so they can be connected 39393 // to a video element (a swf object) 39394 // TODO: can we store this somewhere local to this module? 39395 videojs$1.mediaSources = {}; 39396 39397 /** 39398 * Provide a method for a swf object to notify JS that a 39399 * media source is now open. 39400 * 39401 * @param {String} msObjectURL string referencing the MSE Object URL 39402 * @param {String} swfId the swf id 39403 */ 39404 var open = function open(msObjectURL, swfId) { 39405 var mediaSource = videojs$1.mediaSources[msObjectURL]; 39406 39407 if (mediaSource) { 39408 mediaSource.trigger({ type: 'sourceopen', swfId: swfId }); 39409 } else { 39410 throw new Error('Media Source not found (Video.js)'); 39411 } 39412 }; 39413 39414 /** 39415 * Check to see if the native MediaSource object exists and supports 39416 * an MP4 container with both H.264 video and AAC-LC audio. 39417 * 39418 * @return {Boolean} if native media sources are supported 39419 */ 39420 var supportsNativeMediaSources = function supportsNativeMediaSources() { 39421 return !!window_1.MediaSource && !!window_1.MediaSource.isTypeSupported && window_1.MediaSource.isTypeSupported('video/mp4;codecs="avc1.4d400d,mp4a.40.2"'); 39422 }; 39423 39424 /** 39425 * An emulation of the MediaSource API so that we can support 39426 * native and non-native functionality. returns an instance of 39427 * HtmlMediaSource. 39428 * 39429 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/MediaSource 39430 */ 39431 var MediaSource = function MediaSource() { 39432 this.MediaSource = { 39433 open: open, 39434 supportsNativeMediaSources: supportsNativeMediaSources 39435 }; 39436 39437 if (supportsNativeMediaSources()) { 39438 return new HtmlMediaSource(); 39439 } 39440 39441 throw new Error('Cannot use create a virtual MediaSource for this video'); 39442 }; 39443 39444 MediaSource.open = open; 39445 MediaSource.supportsNativeMediaSources = supportsNativeMediaSources; 39446 39447 /** 39448 * A wrapper around the native URL for our MSE object 39449 * implementation, this object is exposed under videojs.URL 39450 * 39451 * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/URL 39452 */ 39453 var URL$1 = { 39454 /** 39455 * A wrapper around the native createObjectURL for our objects. 39456 * This function maps a native or emulated mediaSource to a blob 39457 * url so that it can be loaded into video.js 39458 * 39459 * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL 39460 * @param {MediaSource} object the object to create a blob url to 39461 */ 39462 createObjectURL: function createObjectURL(object) { 39463 var objectUrlPrefix = 'blob:vjs-media-source/'; 39464 var url = void 0; 39465 39466 // use the native MediaSource to generate an object URL 39467 if (object instanceof HtmlMediaSource) { 39468 url = window_1.URL.createObjectURL(object.nativeMediaSource_); 39469 object.url_ = url; 39470 return url; 39471 } 39472 // if the object isn't an emulated MediaSource, delegate to the 39473 // native implementation 39474 if (!(object instanceof HtmlMediaSource)) { 39475 url = window_1.URL.createObjectURL(object); 39476 object.url_ = url; 39477 return url; 39478 } 39479 39480 // build a URL that can be used to map back to the emulated 39481 // MediaSource 39482 url = objectUrlPrefix + urlCount; 39483 39484 urlCount++; 39485 39486 // setup the mapping back to object 39487 videojs$1.mediaSources[url] = object; 39488 39489 return url; 39490 } 39491 }; 39492 39493 videojs$1.MediaSource = MediaSource; 39494 videojs$1.URL = URL$1; 39495 39496 var EventTarget$1$1 = videojs$1.EventTarget, 39497 mergeOptions$2 = videojs$1.mergeOptions; 39498 39499 /** 39500 * Returns a new master manifest that is the result of merging an updated master manifest 39501 * into the original version. 39502 * 39503 * @param {Object} oldMaster 39504 * The old parsed mpd object 39505 * @param {Object} newMaster 39506 * The updated parsed mpd object 39507 * @return {Object} 39508 * A new object representing the original master manifest with the updated media 39509 * playlists merged in 39510 */ 39511 39512 var updateMaster$1 = function updateMaster$$1(oldMaster, newMaster) { 39513 var update = mergeOptions$2(oldMaster, { 39514 // These are top level properties that can be updated 39515 duration: newMaster.duration, 39516 minimumUpdatePeriod: newMaster.minimumUpdatePeriod 39517 }); 39518 39519 // First update the playlists in playlist list 39520 for (var i = 0; i < newMaster.playlists.length; i++) { 39521 var playlistUpdate = updateMaster(update, newMaster.playlists[i]); 39522 39523 if (playlistUpdate) { 39524 update = playlistUpdate; 39525 } 39526 } 39527 39528 // Then update media group playlists 39529 forEachMediaGroup(newMaster, function (properties, type, group, label) { 39530 if (properties.playlists && properties.playlists.length) { 39531 var uri = properties.playlists[0].uri; 39532 var _playlistUpdate = updateMaster(update, properties.playlists[0]); 39533 39534 if (_playlistUpdate) { 39535 update = _playlistUpdate; 39536 // update the playlist reference within media groups 39537 update.mediaGroups[type][group][label].playlists[0] = update.playlists[uri]; 39538 } 39539 } 39540 }); 39541 39542 return update; 39543 }; 39544 39545 var DashPlaylistLoader = function (_EventTarget) { 39546 inherits$3(DashPlaylistLoader, _EventTarget); 39547 39548 // DashPlaylistLoader must accept either a src url or a playlist because subsequent 39549 // playlist loader setups from media groups will expect to be able to pass a playlist 39550 // (since there aren't external URLs to media playlists with DASH) 39551 function DashPlaylistLoader(srcUrlOrPlaylist, hls, withCredentials, masterPlaylistLoader) { 39552 classCallCheck$3(this, DashPlaylistLoader); 39553 39554 var _this = possibleConstructorReturn$3(this, (DashPlaylistLoader.__proto__ || Object.getPrototypeOf(DashPlaylistLoader)).call(this)); 39555 39556 _this.hls_ = hls; 39557 _this.withCredentials = withCredentials; 39558 39559 if (!srcUrlOrPlaylist) { 39560 throw new Error('A non-empty playlist URL or playlist is required'); 39561 } 39562 39563 // event naming? 39564 _this.on('minimumUpdatePeriod', function () { 39565 _this.refreshXml_(); 39566 }); 39567 39568 // live playlist staleness timeout 39569 _this.on('mediaupdatetimeout', function () { 39570 _this.refreshMedia_(); 39571 }); 39572 39573 // initialize the loader state 39574 if (typeof srcUrlOrPlaylist === 'string') { 39575 _this.srcUrl = srcUrlOrPlaylist; 39576 _this.state = 'HAVE_NOTHING'; 39577 return possibleConstructorReturn$3(_this); 39578 } 39579 39580 _this.masterPlaylistLoader_ = masterPlaylistLoader; 39581 39582 _this.state = 'HAVE_METADATA'; 39583 _this.started = true; 39584 // we only should have one playlist so select it 39585 _this.media(srcUrlOrPlaylist); 39586 // trigger async to mimic behavior of HLS, where it must request a playlist 39587 window_1.setTimeout(function () { 39588 _this.trigger('loadedmetadata'); 39589 }, 0); 39590 return _this; 39591 } 39592 39593 createClass$2(DashPlaylistLoader, [{ 39594 key: 'dispose', 39595 value: function dispose() { 39596 this.stopRequest(); 39597 window_1.clearTimeout(this.mediaUpdateTimeout); 39598 } 39599 }, { 39600 key: 'stopRequest', 39601 value: function stopRequest() { 39602 if (this.request) { 39603 var oldRequest = this.request; 39604 39605 this.request = null; 39606 oldRequest.onreadystatechange = null; 39607 oldRequest.abort(); 39608 } 39609 } 39610 }, { 39611 key: 'media', 39612 value: function media(playlist) { 39613 // getter 39614 if (!playlist) { 39615 return this.media_; 39616 } 39617 39618 // setter 39619 if (this.state === 'HAVE_NOTHING') { 39620 throw new Error('Cannot switch media playlist from ' + this.state); 39621 } 39622 39623 var startingState = this.state; 39624 39625 // find the playlist object if the target playlist has been specified by URI 39626 if (typeof playlist === 'string') { 39627 if (!this.master.playlists[playlist]) { 39628 throw new Error('Unknown playlist URI: ' + playlist); 39629 } 39630 playlist = this.master.playlists[playlist]; 39631 } 39632 39633 var mediaChange = !this.media_ || playlist.uri !== this.media_.uri; 39634 39635 this.state = 'HAVE_METADATA'; 39636 39637 // switching to the active playlist is a no-op 39638 if (!mediaChange) { 39639 return; 39640 } 39641 39642 // switching from an already loaded playlist 39643 if (this.media_) { 39644 this.trigger('mediachanging'); 39645 } 39646 39647 this.media_ = playlist; 39648 39649 this.refreshMedia_(); 39650 39651 // trigger media change if the active media has been updated 39652 if (startingState !== 'HAVE_MASTER') { 39653 this.trigger('mediachange'); 39654 } 39655 } 39656 }, { 39657 key: 'pause', 39658 value: function pause() { 39659 this.stopRequest(); 39660 if (this.state === 'HAVE_NOTHING') { 39661 // If we pause the loader before any data has been retrieved, its as if we never 39662 // started, so reset to an unstarted state. 39663 this.started = false; 39664 } 39665 } 39666 }, { 39667 key: 'load', 39668 value: function load() { 39669 // because the playlists are internal to the manifest, load should either load the 39670 // main manifest, or do nothing but trigger an event 39671 if (!this.started) { 39672 this.start(); 39673 return; 39674 } 39675 39676 this.trigger('loadedplaylist'); 39677 } 39678 39679 /** 39680 * Parses the master xml string and updates playlist uri references 39681 * 39682 * @return {Object} 39683 * The parsed mpd manifest object 39684 */ 39685 39686 }, { 39687 key: 'parseMasterXml', 39688 value: function parseMasterXml() { 39689 var master = parse(this.masterXml_, { 39690 manifestUri: this.srcUrl, 39691 clientOffset: this.clientOffset_ 39692 }); 39693 39694 master.uri = this.srcUrl; 39695 39696 // Set up phony URIs for the playlists since we won't have external URIs for DASH 39697 // but reference playlists by their URI throughout the project 39698 // TODO: Should we create the dummy uris in mpd-parser as well (leaning towards yes). 39699 for (var i = 0; i < master.playlists.length; i++) { 39700 var phonyUri = 'placeholder-uri-' + i; 39701 39702 master.playlists[i].uri = phonyUri; 39703 // set up by URI references 39704 master.playlists[phonyUri] = master.playlists[i]; 39705 } 39706 39707 // set up phony URIs for the media group playlists since we won't have external 39708 // URIs for DASH but reference playlists by their URI throughout the project 39709 forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) { 39710 if (properties.playlists && properties.playlists.length) { 39711 var _phonyUri = 'placeholder-uri-' + mediaType + '-' + groupKey + '-' + labelKey; 39712 39713 properties.playlists[0].uri = _phonyUri; 39714 // setup URI references 39715 master.playlists[_phonyUri] = properties.playlists[0]; 39716 } 39717 }); 39718 39719 setupMediaPlaylists(master); 39720 resolveMediaGroupUris(master); 39721 39722 return master; 39723 } 39724 }, { 39725 key: 'start', 39726 value: function start() { 39727 var _this2 = this; 39728 39729 this.started = true; 39730 39731 // request the specified URL 39732 this.request = this.hls_.xhr({ 39733 uri: this.srcUrl, 39734 withCredentials: this.withCredentials 39735 }, function (error, req) { 39736 // disposed 39737 if (!_this2.request) { 39738 return; 39739 } 39740 39741 // clear the loader's request reference 39742 _this2.request = null; 39743 39744 if (error) { 39745 _this2.error = { 39746 status: req.status, 39747 message: 'DASH playlist request error at URL: ' + _this2.srcUrl, 39748 responseText: req.responseText, 39749 // MEDIA_ERR_NETWORK 39750 code: 2 39751 }; 39752 if (_this2.state === 'HAVE_NOTHING') { 39753 _this2.started = false; 39754 } 39755 return _this2.trigger('error'); 39756 } 39757 39758 _this2.masterXml_ = req.responseText; 39759 39760 if (req.responseHeaders && req.responseHeaders.date) { 39761 _this2.masterLoaded_ = Date.parse(req.responseHeaders.date); 39762 } else { 39763 _this2.masterLoaded_ = Date.now(); 39764 } 39765 39766 _this2.syncClientServerClock_(_this2.onClientServerClockSync_.bind(_this2)); 39767 }); 39768 } 39769 39770 /** 39771 * Parses the master xml for UTCTiming node to sync the client clock to the server 39772 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 39773 * 39774 * @param {Function} done 39775 * Function to call when clock sync has completed 39776 */ 39777 39778 }, { 39779 key: 'syncClientServerClock_', 39780 value: function syncClientServerClock_(done) { 39781 var _this3 = this; 39782 39783 var utcTiming = parseUTCTiming(this.masterXml_); 39784 39785 // No UTCTiming element found in the mpd. Use Date header from mpd request as the 39786 // server clock 39787 if (utcTiming === null) { 39788 this.clientOffset_ = this.masterLoaded_ - Date.now(); 39789 return done(); 39790 } 39791 39792 if (utcTiming.method === 'DIRECT') { 39793 this.clientOffset_ = utcTiming.value - Date.now(); 39794 return done(); 39795 } 39796 39797 this.request = this.hls_.xhr({ 39798 uri: resolveUrl$1(this.srcUrl, utcTiming.value), 39799 method: utcTiming.method, 39800 withCredentials: this.withCredentials 39801 }, function (error, req) { 39802 // disposed 39803 if (!_this3.request) { 39804 return; 39805 } 39806 39807 if (error) { 39808 // sync request failed, fall back to using date header from mpd 39809 // TODO: log warning 39810 _this3.clientOffset_ = _this3.masterLoaded_ - Date.now(); 39811 return done(); 39812 } 39813 39814 var serverTime = void 0; 39815 39816 if (utcTiming.method === 'HEAD') { 39817 if (!req.responseHeaders || !req.responseHeaders.date) { 39818 // expected date header not preset, fall back to using date header from mpd 39819 // TODO: log warning 39820 serverTime = _this3.masterLoaded_; 39821 } else { 39822 serverTime = Date.parse(req.responseHeaders.date); 39823 } 39824 } else { 39825 serverTime = Date.parse(req.responseText); 39826 } 39827 39828 _this3.clientOffset_ = serverTime - Date.now(); 39829 39830 done(); 39831 }); 39832 } 39833 39834 /** 39835 * Handler for after client/server clock synchronization has happened. Sets up 39836 * xml refresh timer if specificed by the manifest. 39837 */ 39838 39839 }, { 39840 key: 'onClientServerClockSync_', 39841 value: function onClientServerClockSync_() { 39842 var _this4 = this; 39843 39844 this.master = this.parseMasterXml(); 39845 39846 this.state = 'HAVE_MASTER'; 39847 39848 this.trigger('loadedplaylist'); 39849 39850 if (!this.media_) { 39851 // no media playlist was specifically selected so start 39852 // from the first listed one 39853 this.media(this.master.playlists[0]); 39854 } 39855 // trigger loadedmetadata to resolve setup of media groups 39856 // trigger async to mimic behavior of HLS, where it must request a playlist 39857 window_1.setTimeout(function () { 39858 _this4.trigger('loadedmetadata'); 39859 }, 0); 39860 39861 // TODO: minimumUpdatePeriod can have a value of 0. Currently the manifest will not 39862 // be refreshed when this is the case. The inter-op guide says that when the 39863 // minimumUpdatePeriod is 0, the manifest should outline all currently available 39864 // segments, but future segments may require an update. I think a good solution 39865 // would be to update the manifest at the same rate that the media playlists 39866 // are "refreshed", i.e. every targetDuration. 39867 if (this.master.minimumUpdatePeriod) { 39868 window_1.setTimeout(function () { 39869 _this4.trigger('minimumUpdatePeriod'); 39870 }, this.master.minimumUpdatePeriod); 39871 } 39872 } 39873 39874 /** 39875 * Sends request to refresh the master xml and updates the parsed master manifest 39876 * TODO: Does the client offset need to be recalculated when the xml is refreshed? 39877 */ 39878 39879 }, { 39880 key: 'refreshXml_', 39881 value: function refreshXml_() { 39882 var _this5 = this; 39883 39884 this.request = this.hls_.xhr({ 39885 uri: this.srcUrl, 39886 withCredentials: this.withCredentials 39887 }, function (error, req) { 39888 // disposed 39889 if (!_this5.request) { 39890 return; 39891 } 39892 39893 // clear the loader's request reference 39894 _this5.request = null; 39895 39896 if (error) { 39897 _this5.error = { 39898 status: req.status, 39899 message: 'DASH playlist request error at URL: ' + _this5.srcUrl, 39900 responseText: req.responseText, 39901 // MEDIA_ERR_NETWORK 39902 code: 2 39903 }; 39904 if (_this5.state === 'HAVE_NOTHING') { 39905 _this5.started = false; 39906 } 39907 return _this5.trigger('error'); 39908 } 39909 39910 _this5.masterXml_ = req.responseText; 39911 39912 var newMaster = _this5.parseMasterXml(); 39913 39914 _this5.master = updateMaster$1(_this5.master, newMaster); 39915 39916 window_1.setTimeout(function () { 39917 _this5.trigger('minimumUpdatePeriod'); 39918 }, _this5.master.minimumUpdatePeriod); 39919 }); 39920 } 39921 39922 /** 39923 * Refreshes the media playlist by re-parsing the master xml and updating playlist 39924 * references. If this is an alternate loader, the updated parsed manifest is retrieved 39925 * from the master loader. 39926 */ 39927 39928 }, { 39929 key: 'refreshMedia_', 39930 value: function refreshMedia_() { 39931 var _this6 = this; 39932 39933 var oldMaster = void 0; 39934 var newMaster = void 0; 39935 39936 if (this.masterPlaylistLoader_) { 39937 oldMaster = this.masterPlaylistLoader_.master; 39938 newMaster = this.masterPlaylistLoader_.parseMasterXml(); 39939 } else { 39940 oldMaster = this.master; 39941 newMaster = this.parseMasterXml(); 39942 } 39943 39944 var updatedMaster = updateMaster$1(oldMaster, newMaster); 39945 39946 if (updatedMaster) { 39947 if (this.masterPlaylistLoader_) { 39948 this.masterPlaylistLoader_.master = updatedMaster; 39949 } else { 39950 this.master = updatedMaster; 39951 } 39952 this.media_ = updatedMaster.playlists[this.media_.uri]; 39953 } else { 39954 this.trigger('playlistunchanged'); 39955 } 39956 39957 if (!this.media().endList) { 39958 this.mediaUpdateTimeout = window_1.setTimeout(function () { 39959 _this6.trigger('mediaupdatetimeout'); 39960 }, refreshDelay(this.media(), !!updatedMaster)); 39961 } 39962 39963 this.trigger('loadedplaylist'); 39964 } 39965 }]); 39966 return DashPlaylistLoader; 39967 }(EventTarget$1$1); 39968 39969 var logger = function logger(source) { 39970 if (videojs$1.log.debug) { 39971 return videojs$1.log.debug.bind(videojs$1, 'VHS:', source + ' >'); 39972 } 39973 39974 return function () {}; 39975 }; 39976 39977 function noop$1() {} 39978 39979 /** 39980 * @file source-updater.js 39981 */ 39982 39983 /** 39984 * A queue of callbacks to be serialized and applied when a 39985 * MediaSource and its associated SourceBuffers are not in the 39986 * updating state. It is used by the segment loader to update the 39987 * underlying SourceBuffers when new data is loaded, for instance. 39988 * 39989 * @class SourceUpdater 39990 * @param {MediaSource} mediaSource the MediaSource to create the 39991 * SourceBuffer from 39992 * @param {String} mimeType the desired MIME type of the underlying 39993 * SourceBuffer 39994 * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer is 39995 * added to the media source 39996 */ 39997 39998 var SourceUpdater = function () { 39999 function SourceUpdater(mediaSource, mimeType, type, sourceBufferEmitter) { 40000 classCallCheck$3(this, SourceUpdater); 40001 40002 this.callbacks_ = []; 40003 this.pendingCallback_ = null; 40004 this.timestampOffset_ = 0; 40005 this.mediaSource = mediaSource; 40006 this.processedAppend_ = false; 40007 this.type_ = type; 40008 this.mimeType_ = mimeType; 40009 this.logger_ = logger('SourceUpdater[' + type + '][' + mimeType + ']'); 40010 40011 if (mediaSource.readyState === 'closed') { 40012 mediaSource.addEventListener('sourceopen', this.createSourceBuffer_.bind(this, mimeType, sourceBufferEmitter)); 40013 } else { 40014 this.createSourceBuffer_(mimeType, sourceBufferEmitter); 40015 } 40016 } 40017 40018 createClass$2(SourceUpdater, [{ 40019 key: 'createSourceBuffer_', 40020 value: function createSourceBuffer_(mimeType, sourceBufferEmitter) { 40021 var _this = this; 40022 40023 this.sourceBuffer_ = this.mediaSource.addSourceBuffer(mimeType); 40024 40025 this.logger_('created SourceBuffer'); 40026 40027 if (sourceBufferEmitter) { 40028 sourceBufferEmitter.trigger('sourcebufferadded'); 40029 40030 if (this.mediaSource.sourceBuffers.length < 2) { 40031 // There's another source buffer we must wait for before we can start updating 40032 // our own (or else we can get into a bad state, i.e., appending video/audio data 40033 // before the other video/audio source buffer is available and leading to a video 40034 // or audio only buffer). 40035 sourceBufferEmitter.on('sourcebufferadded', function () { 40036 _this.start_(); 40037 }); 40038 return; 40039 } 40040 } 40041 40042 this.start_(); 40043 } 40044 }, { 40045 key: 'start_', 40046 value: function start_() { 40047 var _this2 = this; 40048 40049 this.started_ = true; 40050 40051 // run completion handlers and process callbacks as updateend 40052 // events fire 40053 this.onUpdateendCallback_ = function () { 40054 var pendingCallback = _this2.pendingCallback_; 40055 40056 _this2.pendingCallback_ = null; 40057 40058 _this2.logger_('buffered [' + printableRange(_this2.buffered()) + ']'); 40059 40060 if (pendingCallback) { 40061 pendingCallback(); 40062 } 40063 40064 _this2.runCallback_(); 40065 }; 40066 40067 this.sourceBuffer_.addEventListener('updateend', this.onUpdateendCallback_); 40068 40069 this.runCallback_(); 40070 } 40071 40072 /** 40073 * Aborts the current segment and resets the segment parser. 40074 * 40075 * @param {Function} done function to call when done 40076 * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void 40077 */ 40078 40079 }, { 40080 key: 'abort', 40081 value: function abort(done) { 40082 var _this3 = this; 40083 40084 if (this.processedAppend_) { 40085 this.queueCallback_(function () { 40086 _this3.sourceBuffer_.abort(); 40087 }, done); 40088 } 40089 } 40090 40091 /** 40092 * Queue an update to append an ArrayBuffer. 40093 * 40094 * @param {ArrayBuffer} bytes 40095 * @param {Function} done the function to call when done 40096 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 40097 */ 40098 40099 }, { 40100 key: 'appendBuffer', 40101 value: function appendBuffer(bytes, done) { 40102 var _this4 = this; 40103 40104 this.processedAppend_ = true; 40105 this.queueCallback_(function () { 40106 _this4.sourceBuffer_.appendBuffer(bytes); 40107 }, done); 40108 } 40109 40110 /** 40111 * Indicates what TimeRanges are buffered in the managed SourceBuffer. 40112 * 40113 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered 40114 */ 40115 40116 }, { 40117 key: 'buffered', 40118 value: function buffered() { 40119 if (!this.sourceBuffer_) { 40120 return videojs$1.createTimeRanges(); 40121 } 40122 return this.sourceBuffer_.buffered; 40123 } 40124 40125 /** 40126 * Queue an update to remove a time range from the buffer. 40127 * 40128 * @param {Number} start where to start the removal 40129 * @param {Number} end where to end the removal 40130 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 40131 */ 40132 40133 }, { 40134 key: 'remove', 40135 value: function remove(start, end) { 40136 var _this5 = this; 40137 40138 if (this.processedAppend_) { 40139 this.queueCallback_(function () { 40140 _this5.logger_('remove [' + start + ' => ' + end + ']'); 40141 _this5.sourceBuffer_.remove(start, end); 40142 }, noop$1); 40143 } 40144 } 40145 40146 /** 40147 * Whether the underlying sourceBuffer is updating or not 40148 * 40149 * @return {Boolean} the updating status of the SourceBuffer 40150 */ 40151 40152 }, { 40153 key: 'updating', 40154 value: function updating() { 40155 return !this.sourceBuffer_ || this.sourceBuffer_.updating || this.pendingCallback_; 40156 } 40157 40158 /** 40159 * Set/get the timestampoffset on the SourceBuffer 40160 * 40161 * @return {Number} the timestamp offset 40162 */ 40163 40164 }, { 40165 key: 'timestampOffset', 40166 value: function timestampOffset(offset) { 40167 var _this6 = this; 40168 40169 if (typeof offset !== 'undefined') { 40170 this.queueCallback_(function () { 40171 _this6.sourceBuffer_.timestampOffset = offset; 40172 }); 40173 this.timestampOffset_ = offset; 40174 } 40175 return this.timestampOffset_; 40176 } 40177 40178 /** 40179 * Queue a callback to run 40180 */ 40181 40182 }, { 40183 key: 'queueCallback_', 40184 value: function queueCallback_(callback, done) { 40185 this.callbacks_.push([callback.bind(this), done]); 40186 this.runCallback_(); 40187 } 40188 40189 /** 40190 * Run a queued callback 40191 */ 40192 40193 }, { 40194 key: 'runCallback_', 40195 value: function runCallback_() { 40196 var callbacks = void 0; 40197 40198 if (!this.updating() && this.callbacks_.length && this.started_) { 40199 callbacks = this.callbacks_.shift(); 40200 this.pendingCallback_ = callbacks[1]; 40201 callbacks[0](); 40202 } 40203 } 40204 40205 /** 40206 * dispose of the source updater and the underlying sourceBuffer 40207 */ 40208 40209 }, { 40210 key: 'dispose', 40211 value: function dispose() { 40212 this.sourceBuffer_.removeEventListener('updateend', this.onUpdateendCallback_); 40213 if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') { 40214 this.sourceBuffer_.abort(); 40215 } 40216 } 40217 }]); 40218 return SourceUpdater; 40219 }(); 40220 40221 var Config = { 40222 GOAL_BUFFER_LENGTH: 30, 40223 MAX_GOAL_BUFFER_LENGTH: 60, 40224 GOAL_BUFFER_LENGTH_RATE: 1, 40225 // A fudge factor to apply to advertised playlist bitrates to account for 40226 // temporary flucations in client bandwidth 40227 BANDWIDTH_VARIANCE: 1.2, 40228 // How much of the buffer must be filled before we consider upswitching 40229 BUFFER_LOW_WATER_LINE: 0, 40230 MAX_BUFFER_LOW_WATER_LINE: 30, 40231 BUFFER_LOW_WATER_LINE_RATE: 1 40232 }; 40233 40234 var REQUEST_ERRORS = { 40235 FAILURE: 2, 40236 TIMEOUT: -101, 40237 ABORTED: -102 40238 }; 40239 40240 /** 40241 * Turns segment byterange into a string suitable for use in 40242 * HTTP Range requests 40243 * 40244 * @param {Object} byterange - an object with two values defining the start and end 40245 * of a byte-range 40246 */ 40247 var byterangeStr = function byterangeStr(byterange) { 40248 var byterangeStart = void 0; 40249 var byterangeEnd = void 0; 40250 40251 // `byterangeEnd` is one less than `offset + length` because the HTTP range 40252 // header uses inclusive ranges 40253 byterangeEnd = byterange.offset + byterange.length - 1; 40254 byterangeStart = byterange.offset; 40255 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 40256 }; 40257 40258 /** 40259 * Defines headers for use in the xhr request for a particular segment. 40260 * 40261 * @param {Object} segment - a simplified copy of the segmentInfo object 40262 * from SegmentLoader 40263 */ 40264 var segmentXhrHeaders = function segmentXhrHeaders(segment) { 40265 var headers = {}; 40266 40267 if (segment.byterange) { 40268 headers.Range = byterangeStr(segment.byterange); 40269 } 40270 return headers; 40271 }; 40272 40273 /** 40274 * Abort all requests 40275 * 40276 * @param {Object} activeXhrs - an object that tracks all XHR requests 40277 */ 40278 var abortAll = function abortAll(activeXhrs) { 40279 activeXhrs.forEach(function (xhr) { 40280 xhr.abort(); 40281 }); 40282 }; 40283 40284 /** 40285 * Gather important bandwidth stats once a request has completed 40286 * 40287 * @param {Object} request - the XHR request from which to gather stats 40288 */ 40289 var getRequestStats = function getRequestStats(request) { 40290 return { 40291 bandwidth: request.bandwidth, 40292 bytesReceived: request.bytesReceived || 0, 40293 roundTripTime: request.roundTripTime || 0 40294 }; 40295 }; 40296 40297 /** 40298 * If possible gather bandwidth stats as a request is in 40299 * progress 40300 * 40301 * @param {Event} progressEvent - an event object from an XHR's progress event 40302 */ 40303 var getProgressStats = function getProgressStats(progressEvent) { 40304 var request = progressEvent.target; 40305 var roundTripTime = Date.now() - request.requestTime; 40306 var stats = { 40307 bandwidth: Infinity, 40308 bytesReceived: 0, 40309 roundTripTime: roundTripTime || 0 40310 }; 40311 40312 stats.bytesReceived = progressEvent.loaded; 40313 // This can result in Infinity if stats.roundTripTime is 0 but that is ok 40314 // because we should only use bandwidth stats on progress to determine when 40315 // abort a request early due to insufficient bandwidth 40316 stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); 40317 40318 return stats; 40319 }; 40320 40321 /** 40322 * Handle all error conditions in one place and return an object 40323 * with all the information 40324 * 40325 * @param {Error|null} error - if non-null signals an error occured with the XHR 40326 * @param {Object} request - the XHR request that possibly generated the error 40327 */ 40328 var handleErrors = function handleErrors(error, request) { 40329 if (request.timedout) { 40330 return { 40331 status: request.status, 40332 message: 'HLS request timed-out at URL: ' + request.uri, 40333 code: REQUEST_ERRORS.TIMEOUT, 40334 xhr: request 40335 }; 40336 } 40337 40338 if (request.aborted) { 40339 return { 40340 status: request.status, 40341 message: 'HLS request aborted at URL: ' + request.uri, 40342 code: REQUEST_ERRORS.ABORTED, 40343 xhr: request 40344 }; 40345 } 40346 40347 if (error) { 40348 return { 40349 status: request.status, 40350 message: 'HLS request errored at URL: ' + request.uri, 40351 code: REQUEST_ERRORS.FAILURE, 40352 xhr: request 40353 }; 40354 } 40355 40356 return null; 40357 }; 40358 40359 /** 40360 * Handle responses for key data and convert the key data to the correct format 40361 * for the decryption step later 40362 * 40363 * @param {Object} segment - a simplified copy of the segmentInfo object 40364 * from SegmentLoader 40365 * @param {Function} finishProcessingFn - a callback to execute to continue processing 40366 * this request 40367 */ 40368 var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn) { 40369 return function (error, request) { 40370 var response = request.response; 40371 var errorObj = handleErrors(error, request); 40372 40373 if (errorObj) { 40374 return finishProcessingFn(errorObj, segment); 40375 } 40376 40377 if (response.byteLength !== 16) { 40378 return finishProcessingFn({ 40379 status: request.status, 40380 message: 'Invalid HLS key at URL: ' + request.uri, 40381 code: REQUEST_ERRORS.FAILURE, 40382 xhr: request 40383 }, segment); 40384 } 40385 40386 var view = new DataView(response); 40387 40388 segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); 40389 return finishProcessingFn(null, segment); 40390 }; 40391 }; 40392 40393 /** 40394 * Handle init-segment responses 40395 * 40396 * @param {Object} segment - a simplified copy of the segmentInfo object 40397 * from SegmentLoader 40398 * @param {Function} finishProcessingFn - a callback to execute to continue processing 40399 * this request 40400 */ 40401 var handleInitSegmentResponse = function handleInitSegmentResponse(segment, finishProcessingFn) { 40402 return function (error, request) { 40403 var response = request.response; 40404 var errorObj = handleErrors(error, request); 40405 40406 if (errorObj) { 40407 return finishProcessingFn(errorObj, segment); 40408 } 40409 40410 // stop processing if received empty content 40411 if (response.byteLength === 0) { 40412 return finishProcessingFn({ 40413 status: request.status, 40414 message: 'Empty HLS segment content at URL: ' + request.uri, 40415 code: REQUEST_ERRORS.FAILURE, 40416 xhr: request 40417 }, segment); 40418 } 40419 40420 segment.map.bytes = new Uint8Array(request.response); 40421 return finishProcessingFn(null, segment); 40422 }; 40423 }; 40424 40425 /** 40426 * Response handler for segment-requests being sure to set the correct 40427 * property depending on whether the segment is encryped or not 40428 * Also records and keeps track of stats that are used for ABR purposes 40429 * 40430 * @param {Object} segment - a simplified copy of the segmentInfo object 40431 * from SegmentLoader 40432 * @param {Function} finishProcessingFn - a callback to execute to continue processing 40433 * this request 40434 */ 40435 var handleSegmentResponse = function handleSegmentResponse(segment, finishProcessingFn) { 40436 return function (error, request) { 40437 var response = request.response; 40438 var errorObj = handleErrors(error, request); 40439 40440 if (errorObj) { 40441 return finishProcessingFn(errorObj, segment); 40442 } 40443 40444 // stop processing if received empty content 40445 if (response.byteLength === 0) { 40446 return finishProcessingFn({ 40447 status: request.status, 40448 message: 'Empty HLS segment content at URL: ' + request.uri, 40449 code: REQUEST_ERRORS.FAILURE, 40450 xhr: request 40451 }, segment); 40452 } 40453 40454 segment.stats = getRequestStats(request); 40455 40456 if (segment.key) { 40457 segment.encryptedBytes = new Uint8Array(request.response); 40458 } else { 40459 segment.bytes = new Uint8Array(request.response); 40460 } 40461 40462 return finishProcessingFn(null, segment); 40463 }; 40464 }; 40465 40466 /** 40467 * Decrypt the segment via the decryption web worker 40468 * 40469 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 40470 * @param {Object} segment - a simplified copy of the segmentInfo object 40471 * from SegmentLoader 40472 * @param {Function} doneFn - a callback that is executed after decryption has completed 40473 */ 40474 var decryptSegment = function decryptSegment(decrypter, segment, doneFn) { 40475 var decryptionHandler = function decryptionHandler(event) { 40476 if (event.data.source === segment.requestId) { 40477 decrypter.removeEventListener('message', decryptionHandler); 40478 var decrypted = event.data.decrypted; 40479 40480 segment.bytes = new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength); 40481 return doneFn(null, segment); 40482 } 40483 }; 40484 40485 decrypter.addEventListener('message', decryptionHandler); 40486 40487 // this is an encrypted segment 40488 // incrementally decrypt the segment 40489 decrypter.postMessage(createTransferableMessage({ 40490 source: segment.requestId, 40491 encrypted: segment.encryptedBytes, 40492 key: segment.key.bytes, 40493 iv: segment.key.iv 40494 }), [segment.encryptedBytes.buffer, segment.key.bytes.buffer]); 40495 }; 40496 40497 /** 40498 * The purpose of this function is to get the most pertinent error from the 40499 * array of errors. 40500 * For instance if a timeout and two aborts occur, then the aborts were 40501 * likely triggered by the timeout so return that error object. 40502 */ 40503 var getMostImportantError = function getMostImportantError(errors) { 40504 return errors.reduce(function (prev, err) { 40505 return err.code > prev.code ? err : prev; 40506 }); 40507 }; 40508 40509 /** 40510 * This function waits for all XHRs to finish (with either success or failure) 40511 * before continueing processing via it's callback. The function gathers errors 40512 * from each request into a single errors array so that the error status for 40513 * each request can be examined later. 40514 * 40515 * @param {Object} activeXhrs - an object that tracks all XHR requests 40516 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 40517 * @param {Function} doneFn - a callback that is executed after all resources have been 40518 * downloaded and any decryption completed 40519 */ 40520 var waitForCompletion = function waitForCompletion(activeXhrs, decrypter, doneFn) { 40521 var errors = []; 40522 var count = 0; 40523 40524 return function (error, segment) { 40525 if (error) { 40526 // If there are errors, we have to abort any outstanding requests 40527 abortAll(activeXhrs); 40528 errors.push(error); 40529 } 40530 count += 1; 40531 40532 if (count === activeXhrs.length) { 40533 // Keep track of when *all* of the requests have completed 40534 segment.endOfAllRequests = Date.now(); 40535 40536 if (errors.length > 0) { 40537 var worstError = getMostImportantError(errors); 40538 40539 return doneFn(worstError, segment); 40540 } 40541 if (segment.encryptedBytes) { 40542 return decryptSegment(decrypter, segment, doneFn); 40543 } 40544 // Otherwise, everything is ready just continue 40545 return doneFn(null, segment); 40546 } 40547 }; 40548 }; 40549 40550 /** 40551 * Simple progress event callback handler that gathers some stats before 40552 * executing a provided callback with the `segment` object 40553 * 40554 * @param {Object} segment - a simplified copy of the segmentInfo object 40555 * from SegmentLoader 40556 * @param {Function} progressFn - a callback that is executed each time a progress event 40557 * is received 40558 * @param {Event} event - the progress event object from XMLHttpRequest 40559 */ 40560 var handleProgress = function handleProgress(segment, progressFn) { 40561 return function (event) { 40562 segment.stats = videojs$1.mergeOptions(segment.stats, getProgressStats(event)); 40563 40564 // record the time that we receive the first byte of data 40565 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 40566 segment.stats.firstBytesReceivedAt = Date.now(); 40567 } 40568 40569 return progressFn(event, segment); 40570 }; 40571 }; 40572 40573 /** 40574 * Load all resources and does any processing necessary for a media-segment 40575 * 40576 * Features: 40577 * decrypts the media-segment if it has a key uri and an iv 40578 * aborts *all* requests if *any* one request fails 40579 * 40580 * The segment object, at minimum, has the following format: 40581 * { 40582 * resolvedUri: String, 40583 * [byterange]: { 40584 * offset: Number, 40585 * length: Number 40586 * }, 40587 * [key]: { 40588 * resolvedUri: String 40589 * [byterange]: { 40590 * offset: Number, 40591 * length: Number 40592 * }, 40593 * iv: { 40594 * bytes: Uint32Array 40595 * } 40596 * }, 40597 * [map]: { 40598 * resolvedUri: String, 40599 * [byterange]: { 40600 * offset: Number, 40601 * length: Number 40602 * }, 40603 * [bytes]: Uint8Array 40604 * } 40605 * } 40606 * ...where [name] denotes optional properties 40607 * 40608 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 40609 * @param {Object} xhrOptions - the base options to provide to all xhr requests 40610 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 40611 * decryption routines 40612 * @param {Object} segment - a simplified copy of the segmentInfo object 40613 * from SegmentLoader 40614 * @param {Function} progressFn - a callback that receives progress events from the main 40615 * segment's xhr request 40616 * @param {Function} doneFn - a callback that is executed only once all requests have 40617 * succeeded or failed 40618 * @returns {Function} a function that, when invoked, immediately aborts all 40619 * outstanding requests 40620 */ 40621 var mediaSegmentRequest = function mediaSegmentRequest(xhr, xhrOptions, decryptionWorker, segment, progressFn, doneFn) { 40622 var activeXhrs = []; 40623 var finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn); 40624 40625 // optionally, request the decryption key 40626 if (segment.key) { 40627 var keyRequestOptions = videojs$1.mergeOptions(xhrOptions, { 40628 uri: segment.key.resolvedUri, 40629 responseType: 'arraybuffer' 40630 }); 40631 var keyRequestCallback = handleKeyResponse(segment, finishProcessingFn); 40632 var keyXhr = xhr(keyRequestOptions, keyRequestCallback); 40633 40634 activeXhrs.push(keyXhr); 40635 } 40636 40637 // optionally, request the associated media init segment 40638 if (segment.map && !segment.map.bytes) { 40639 var initSegmentOptions = videojs$1.mergeOptions(xhrOptions, { 40640 uri: segment.map.resolvedUri, 40641 responseType: 'arraybuffer', 40642 headers: segmentXhrHeaders(segment.map) 40643 }); 40644 var initSegmentRequestCallback = handleInitSegmentResponse(segment, finishProcessingFn); 40645 var initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 40646 40647 activeXhrs.push(initSegmentXhr); 40648 } 40649 40650 var segmentRequestOptions = videojs$1.mergeOptions(xhrOptions, { 40651 uri: segment.resolvedUri, 40652 responseType: 'arraybuffer', 40653 headers: segmentXhrHeaders(segment) 40654 }); 40655 var segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn); 40656 var segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 40657 40658 segmentXhr.addEventListener('progress', handleProgress(segment, progressFn)); 40659 activeXhrs.push(segmentXhr); 40660 40661 return function () { 40662 return abortAll(activeXhrs); 40663 }; 40664 }; 40665 40666 /** 40667 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 40668 * codec strings, or translating codec strings into objects that can be examined. 40669 */ 40670 40671 // Default codec parameters if none were provided for video and/or audio 40672 var defaultCodecs = { 40673 videoCodec: 'avc1', 40674 videoObjectTypeIndicator: '.4d400d', 40675 // AAC-LC 40676 audioProfile: '2' 40677 }; 40678 40679 /** 40680 * Parses a codec string to retrieve the number of codecs specified, 40681 * the video codec and object type indicator, and the audio profile. 40682 */ 40683 40684 var parseCodecs = function parseCodecs() { 40685 var codecs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 40686 40687 var result = { 40688 codecCount: 0 40689 }; 40690 var parsed = void 0; 40691 40692 result.codecCount = codecs.split(',').length; 40693 result.codecCount = result.codecCount || 2; 40694 40695 // parse the video codec 40696 parsed = /(^|\s|,)+(avc[13])([^ ,]*)/i.exec(codecs); 40697 if (parsed) { 40698 result.videoCodec = parsed[2]; 40699 result.videoObjectTypeIndicator = parsed[3]; 40700 } 40701 40702 // parse the last field of the audio codec 40703 result.audioProfile = /(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i.exec(codecs); 40704 result.audioProfile = result.audioProfile && result.audioProfile[2]; 40705 40706 return result; 40707 }; 40708 40709 /** 40710 * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the 40711 * standard `avc1.<hhhhhh>`. 40712 * 40713 * @param codecString {String} the codec string 40714 * @return {String} the codec string with old apple-style codecs replaced 40715 * 40716 * @private 40717 */ 40718 var mapLegacyAvcCodecs = function mapLegacyAvcCodecs(codecString) { 40719 return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) { 40720 return translateLegacyCodecs([match])[0]; 40721 }); 40722 }; 40723 40724 /** 40725 * Build a media mime-type string from a set of parameters 40726 * @param {String} type either 'audio' or 'video' 40727 * @param {String} container either 'mp2t' or 'mp4' 40728 * @param {Array} codecs an array of codec strings to add 40729 * @return {String} a valid media mime-type 40730 */ 40731 var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) { 40732 // The codecs array is filtered so that falsey values are 40733 // dropped and don't cause Array#join to create spurious 40734 // commas 40735 return type + '/' + container + '; codecs="' + codecs.filter(function (c) { 40736 return !!c; 40737 }).join(', ') + '"'; 40738 }; 40739 40740 /** 40741 * Returns the type container based on information in the playlist 40742 * @param {Playlist} media the current media playlist 40743 * @return {String} a valid media container type 40744 */ 40745 var getContainerType = function getContainerType(media) { 40746 // An initialization segment means the media playlist is an iframe 40747 // playlist or is using the mp4 container. We don't currently 40748 // support iframe playlists, so assume this is signalling mp4 40749 // fragments. 40750 if (media.segments && media.segments.length && media.segments[0].map) { 40751 return 'mp4'; 40752 } 40753 return 'mp2t'; 40754 }; 40755 40756 /** 40757 * Returns a set of codec strings parsed from the playlist or the default 40758 * codec strings if no codecs were specified in the playlist 40759 * @param {Playlist} media the current media playlist 40760 * @return {Object} an object with the video and audio codecs 40761 */ 40762 var getCodecs = function getCodecs(media) { 40763 // if the codecs were explicitly specified, use them instead of the 40764 // defaults 40765 var mediaAttributes = media.attributes || {}; 40766 40767 if (mediaAttributes.CODECS) { 40768 return parseCodecs(mediaAttributes.CODECS); 40769 } 40770 return defaultCodecs; 40771 }; 40772 40773 var audioProfileFromDefault = function audioProfileFromDefault(master, audioGroupId) { 40774 if (!master.mediaGroups.AUDIO || !audioGroupId) { 40775 return null; 40776 } 40777 40778 var audioGroup = master.mediaGroups.AUDIO[audioGroupId]; 40779 40780 if (!audioGroup) { 40781 return null; 40782 } 40783 40784 for (var name in audioGroup) { 40785 var audioType = audioGroup[name]; 40786 40787 if (audioType.default && audioType.playlists) { 40788 // codec should be the same for all playlists within the audio type 40789 return parseCodecs(audioType.playlists[0].attributes.CODECS).audioProfile; 40790 } 40791 } 40792 40793 return null; 40794 }; 40795 40796 /** 40797 * Calculates the MIME type strings for a working configuration of 40798 * SourceBuffers to play variant streams in a master playlist. If 40799 * there is no possible working configuration, an empty array will be 40800 * returned. 40801 * 40802 * @param master {Object} the m3u8 object for the master playlist 40803 * @param media {Object} the m3u8 object for the variant playlist 40804 * @return {Array} the MIME type strings. If the array has more than 40805 * one entry, the first element should be applied to the video 40806 * SourceBuffer and the second to the audio SourceBuffer. 40807 * 40808 * @private 40809 */ 40810 var mimeTypesForPlaylist = function mimeTypesForPlaylist(master, media) { 40811 var containerType = getContainerType(media); 40812 var codecInfo = getCodecs(media); 40813 var mediaAttributes = media.attributes || {}; 40814 // Default condition for a traditional HLS (no demuxed audio/video) 40815 var isMuxed = true; 40816 var isMaat = false; 40817 40818 if (!media) { 40819 // Not enough information 40820 return []; 40821 } 40822 40823 if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) { 40824 var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 40825 40826 // Handle the case where we are in a multiple-audio track scenario 40827 if (audioGroup) { 40828 isMaat = true; 40829 // Start with the everything demuxed then... 40830 isMuxed = false; 40831 // ...check to see if any audio group tracks are muxed (ie. lacking a uri) 40832 for (var groupId in audioGroup) { 40833 // either a uri is present (if the case of HLS and an external playlist), or 40834 // playlists is present (in the case of DASH where we don't have external audio 40835 // playlists) 40836 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 40837 isMuxed = true; 40838 break; 40839 } 40840 } 40841 } 40842 } 40843 40844 // HLS with multiple-audio tracks must always get an audio codec. 40845 // Put another way, there is no way to have a video-only multiple-audio HLS! 40846 if (isMaat && !codecInfo.audioProfile) { 40847 if (!isMuxed) { 40848 // It is possible for codecs to be specified on the audio media group playlist but 40849 // not on the rendition playlist. This is mostly the case for DASH, where audio and 40850 // video are always separate (and separately specified). 40851 codecInfo.audioProfile = audioProfileFromDefault(master, mediaAttributes.AUDIO); 40852 } 40853 40854 if (!codecInfo.audioProfile) { 40855 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)'); 40856 codecInfo.audioProfile = defaultCodecs.audioProfile; 40857 } 40858 } 40859 40860 // Generate the final codec strings from the codec object generated above 40861 var codecStrings = {}; 40862 40863 if (codecInfo.videoCodec) { 40864 codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator; 40865 } 40866 40867 if (codecInfo.audioProfile) { 40868 codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile; 40869 } 40870 40871 // Finally, make and return an array with proper mime-types depending on 40872 // the configuration 40873 var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]); 40874 var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]); 40875 var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]); 40876 40877 if (isMaat) { 40878 if (!isMuxed && codecStrings.video) { 40879 return [justVideo, justAudio]; 40880 } 40881 40882 if (!isMuxed && !codecStrings.video) { 40883 // There is no muxed content and no video codec string, so this is an audio only 40884 // stream with alternate audio. 40885 return [justAudio, justAudio]; 40886 } 40887 40888 // There exists the possiblity that this will return a `video/container` 40889 // mime-type for the first entry in the array even when there is only audio. 40890 // This doesn't appear to be a problem and simplifies the code. 40891 return [bothVideoAudio, justAudio]; 40892 } 40893 40894 // If there is no video codec at all, always just return a single 40895 // audio/<container> mime-type 40896 if (!codecStrings.video) { 40897 return [justAudio]; 40898 } 40899 40900 // When not using separate audio media groups, audio and video is 40901 // *always* muxed 40902 return [bothVideoAudio]; 40903 }; 40904 40905 // Utilities 40906 40907 /** 40908 * Returns the CSS value for the specified property on an element 40909 * using `getComputedStyle`. Firefox has a long-standing issue where 40910 * getComputedStyle() may return null when running in an iframe with 40911 * `display: none`. 40912 * 40913 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 40914 * @param {HTMLElement} el the htmlelement to work on 40915 * @param {string} the proprety to get the style for 40916 */ 40917 var safeGetComputedStyle = function safeGetComputedStyle(el, property) { 40918 var result = void 0; 40919 40920 if (!el) { 40921 return ''; 40922 } 40923 40924 result = window_1.getComputedStyle(el); 40925 if (!result) { 40926 return ''; 40927 } 40928 40929 return result[property]; 40930 }; 40931 40932 /** 40933 * Resuable stable sort function 40934 * 40935 * @param {Playlists} array 40936 * @param {Function} sortFn Different comparators 40937 * @function stableSort 40938 */ 40939 var stableSort = function stableSort(array, sortFn) { 40940 var newArray = array.slice(); 40941 40942 array.sort(function (left, right) { 40943 var cmp = sortFn(left, right); 40944 40945 if (cmp === 0) { 40946 return newArray.indexOf(left) - newArray.indexOf(right); 40947 } 40948 return cmp; 40949 }); 40950 }; 40951 40952 /** 40953 * A comparator function to sort two playlist object by bandwidth. 40954 * 40955 * @param {Object} left a media playlist object 40956 * @param {Object} right a media playlist object 40957 * @return {Number} Greater than zero if the bandwidth attribute of 40958 * left is greater than the corresponding attribute of right. Less 40959 * than zero if the bandwidth of right is greater than left and 40960 * exactly zero if the two are equal. 40961 */ 40962 var comparePlaylistBandwidth = function comparePlaylistBandwidth(left, right) { 40963 var leftBandwidth = void 0; 40964 var rightBandwidth = void 0; 40965 40966 if (left.attributes.BANDWIDTH) { 40967 leftBandwidth = left.attributes.BANDWIDTH; 40968 } 40969 leftBandwidth = leftBandwidth || window_1.Number.MAX_VALUE; 40970 if (right.attributes.BANDWIDTH) { 40971 rightBandwidth = right.attributes.BANDWIDTH; 40972 } 40973 rightBandwidth = rightBandwidth || window_1.Number.MAX_VALUE; 40974 40975 return leftBandwidth - rightBandwidth; 40976 }; 40977 40978 /** 40979 * A comparator function to sort two playlist object by resolution (width). 40980 * @param {Object} left a media playlist object 40981 * @param {Object} right a media playlist object 40982 * @return {Number} Greater than zero if the resolution.width attribute of 40983 * left is greater than the corresponding attribute of right. Less 40984 * than zero if the resolution.width of right is greater than left and 40985 * exactly zero if the two are equal. 40986 */ 40987 var comparePlaylistResolution = function comparePlaylistResolution(left, right) { 40988 var leftWidth = void 0; 40989 var rightWidth = void 0; 40990 40991 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { 40992 leftWidth = left.attributes.RESOLUTION.width; 40993 } 40994 40995 leftWidth = leftWidth || window_1.Number.MAX_VALUE; 40996 40997 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 40998 rightWidth = right.attributes.RESOLUTION.width; 40999 } 41000 41001 rightWidth = rightWidth || window_1.Number.MAX_VALUE; 41002 41003 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 41004 // have the same media dimensions/ resolution 41005 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 41006 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 41007 } 41008 return leftWidth - rightWidth; 41009 }; 41010 41011 /** 41012 * Chooses the appropriate media playlist based on bandwidth and player size 41013 * 41014 * @param {Object} master 41015 * Object representation of the master manifest 41016 * @param {Number} playerBandwidth 41017 * Current calculated bandwidth of the player 41018 * @param {Number} playerWidth 41019 * Current width of the player element 41020 * @param {Number} playerHeight 41021 * Current height of the player element 41022 * @return {Playlist} the highest bitrate playlist less than the 41023 * currently detected bandwidth, accounting for some amount of 41024 * bandwidth variance 41025 */ 41026 var simpleSelector = function simpleSelector(master, playerBandwidth, playerWidth, playerHeight) { 41027 // convert the playlists to an intermediary representation to make comparisons easier 41028 var sortedPlaylistReps = master.playlists.map(function (playlist) { 41029 var width = void 0; 41030 var height = void 0; 41031 var bandwidth = void 0; 41032 41033 width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 41034 height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 41035 bandwidth = playlist.attributes.BANDWIDTH; 41036 41037 bandwidth = bandwidth || window_1.Number.MAX_VALUE; 41038 41039 return { 41040 bandwidth: bandwidth, 41041 width: width, 41042 height: height, 41043 playlist: playlist 41044 }; 41045 }); 41046 41047 stableSort(sortedPlaylistReps, function (left, right) { 41048 return left.bandwidth - right.bandwidth; 41049 }); 41050 41051 // filter out any playlists that have been excluded due to 41052 // incompatible configurations 41053 sortedPlaylistReps = sortedPlaylistReps.filter(function (rep) { 41054 return !Playlist.isIncompatible(rep.playlist); 41055 }); 41056 41057 // filter out any playlists that have been disabled manually through the representations 41058 // api or blacklisted temporarily due to playback errors. 41059 var enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) { 41060 return Playlist.isEnabled(rep.playlist); 41061 }); 41062 41063 if (!enabledPlaylistReps.length) { 41064 // if there are no enabled playlists, then they have all been blacklisted or disabled 41065 // by the user through the representations api. In this case, ignore blacklisting and 41066 // fallback to what the user wants by using playlists the user has not disabled. 41067 enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) { 41068 return !Playlist.isDisabled(rep.playlist); 41069 }); 41070 } 41071 41072 // filter out any variant that has greater effective bitrate 41073 // than the current estimated bandwidth 41074 var bandwidthPlaylistReps = enabledPlaylistReps.filter(function (rep) { 41075 return rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth; 41076 }); 41077 41078 var highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; 41079 41080 // get all of the renditions with the same (highest) bandwidth 41081 // and then taking the very first element 41082 var bandwidthBestRep = bandwidthPlaylistReps.filter(function (rep) { 41083 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 41084 })[0]; 41085 41086 // filter out playlists without resolution information 41087 var haveResolution = bandwidthPlaylistReps.filter(function (rep) { 41088 return rep.width && rep.height; 41089 }); 41090 41091 // sort variants by resolution 41092 stableSort(haveResolution, function (left, right) { 41093 return left.width - right.width; 41094 }); 41095 41096 // if we have the exact resolution as the player use it 41097 var resolutionBestRepList = haveResolution.filter(function (rep) { 41098 return rep.width === playerWidth && rep.height === playerHeight; 41099 }); 41100 41101 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; 41102 // ensure that we pick the highest bandwidth variant that have exact resolution 41103 var resolutionBestRep = resolutionBestRepList.filter(function (rep) { 41104 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 41105 })[0]; 41106 41107 var resolutionPlusOneList = void 0; 41108 var resolutionPlusOneSmallest = void 0; 41109 var resolutionPlusOneRep = void 0; 41110 41111 // find the smallest variant that is larger than the player 41112 // if there is no match of exact resolution 41113 if (!resolutionBestRep) { 41114 resolutionPlusOneList = haveResolution.filter(function (rep) { 41115 return rep.width > playerWidth || rep.height > playerHeight; 41116 }); 41117 41118 // find all the variants have the same smallest resolution 41119 resolutionPlusOneSmallest = resolutionPlusOneList.filter(function (rep) { 41120 return rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height; 41121 }); 41122 41123 // ensure that we also pick the highest bandwidth variant that 41124 // is just-larger-than the video player 41125 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 41126 resolutionPlusOneRep = resolutionPlusOneSmallest.filter(function (rep) { 41127 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 41128 })[0]; 41129 } 41130 41131 // fallback chain of variants 41132 var chosenRep = resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 41133 41134 return chosenRep ? chosenRep.playlist : null; 41135 }; 41136 41137 // Playlist Selectors 41138 41139 /** 41140 * Chooses the appropriate media playlist based on the most recent 41141 * bandwidth estimate and the player size. 41142 * 41143 * Expects to be called within the context of an instance of HlsHandler 41144 * 41145 * @return {Playlist} the highest bitrate playlist less than the 41146 * currently detected bandwidth, accounting for some amount of 41147 * bandwidth variance 41148 */ 41149 var lastBandwidthSelector = function lastBandwidthSelector() { 41150 return simpleSelector(this.playlists.master, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10)); 41151 }; 41152 41153 /** 41154 * Chooses the appropriate media playlist based on the potential to rebuffer 41155 * 41156 * @param {Object} settings 41157 * Object of information required to use this selector 41158 * @param {Object} settings.master 41159 * Object representation of the master manifest 41160 * @param {Number} settings.currentTime 41161 * The current time of the player 41162 * @param {Number} settings.bandwidth 41163 * Current measured bandwidth 41164 * @param {Number} settings.duration 41165 * Duration of the media 41166 * @param {Number} settings.segmentDuration 41167 * Segment duration to be used in round trip time calculations 41168 * @param {Number} settings.timeUntilRebuffer 41169 * Time left in seconds until the player has to rebuffer 41170 * @param {Number} settings.currentTimeline 41171 * The current timeline segments are being loaded from 41172 * @param {SyncController} settings.syncController 41173 * SyncController for determining if we have a sync point for a given playlist 41174 * @return {Object|null} 41175 * {Object} return.playlist 41176 * The highest bandwidth playlist with the least amount of rebuffering 41177 * {Number} return.rebufferingImpact 41178 * The amount of time in seconds switching to this playlist will rebuffer. A 41179 * negative value means that switching will cause zero rebuffering. 41180 */ 41181 var minRebufferMaxBandwidthSelector = function minRebufferMaxBandwidthSelector(settings) { 41182 var master = settings.master, 41183 currentTime = settings.currentTime, 41184 bandwidth = settings.bandwidth, 41185 duration$$1 = settings.duration, 41186 segmentDuration = settings.segmentDuration, 41187 timeUntilRebuffer = settings.timeUntilRebuffer, 41188 currentTimeline = settings.currentTimeline, 41189 syncController = settings.syncController; 41190 41191 // filter out any playlists that have been excluded due to 41192 // incompatible configurations 41193 41194 var compatiblePlaylists = master.playlists.filter(function (playlist) { 41195 return !Playlist.isIncompatible(playlist); 41196 }); 41197 41198 // filter out any playlists that have been disabled manually through the representations 41199 // api or blacklisted temporarily due to playback errors. 41200 var enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 41201 41202 if (!enabledPlaylists.length) { 41203 // if there are no enabled playlists, then they have all been blacklisted or disabled 41204 // by the user through the representations api. In this case, ignore blacklisting and 41205 // fallback to what the user wants by using playlists the user has not disabled. 41206 enabledPlaylists = compatiblePlaylists.filter(function (playlist) { 41207 return !Playlist.isDisabled(playlist); 41208 }); 41209 } 41210 41211 var bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 41212 41213 var rebufferingEstimates = bandwidthPlaylists.map(function (playlist) { 41214 var syncPoint = syncController.getSyncPoint(playlist, duration$$1, currentTimeline, currentTime); 41215 // If there is no sync point for this playlist, switching to it will require a 41216 // sync request first. This will double the request time 41217 var numRequests = syncPoint ? 1 : 2; 41218 var requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist); 41219 var rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 41220 41221 return { 41222 playlist: playlist, 41223 rebufferingImpact: rebufferingImpact 41224 }; 41225 }); 41226 41227 var noRebufferingPlaylists = rebufferingEstimates.filter(function (estimate) { 41228 return estimate.rebufferingImpact <= 0; 41229 }); 41230 41231 // Sort by bandwidth DESC 41232 stableSort(noRebufferingPlaylists, function (a, b) { 41233 return comparePlaylistBandwidth(b.playlist, a.playlist); 41234 }); 41235 41236 if (noRebufferingPlaylists.length) { 41237 return noRebufferingPlaylists[0]; 41238 } 41239 41240 stableSort(rebufferingEstimates, function (a, b) { 41241 return a.rebufferingImpact - b.rebufferingImpact; 41242 }); 41243 41244 return rebufferingEstimates[0] || null; 41245 }; 41246 41247 /** 41248 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 41249 * one with video. If no renditions with video exist, return the lowest audio rendition. 41250 * 41251 * Expects to be called within the context of an instance of HlsHandler 41252 * 41253 * @return {Object|null} 41254 * {Object} return.playlist 41255 * The lowest bitrate playlist that contains a video codec. If no such rendition 41256 * exists pick the lowest audio rendition. 41257 */ 41258 var lowestBitrateCompatibleVariantSelector = function lowestBitrateCompatibleVariantSelector() { 41259 // filter out any playlists that have been excluded due to 41260 // incompatible configurations or playback errors 41261 var playlists = this.playlists.master.playlists.filter(Playlist.isEnabled); 41262 41263 // Sort ascending by bitrate 41264 stableSort(playlists, function (a, b) { 41265 return comparePlaylistBandwidth(a, b); 41266 }); 41267 41268 // Parse and assume that playlists with no video codec have no video 41269 // (this is not necessarily true, although it is generally true). 41270 // 41271 // If an entire manifest has no valid videos everything will get filtered 41272 // out. 41273 var playlistsWithVideo = playlists.filter(function (playlist) { 41274 return parseCodecs(playlist.attributes.CODECS).videoCodec; 41275 }); 41276 41277 return playlistsWithVideo[0] || null; 41278 }; 41279 41280 /** 41281 * @file segment-loader.js 41282 */ 41283 41284 // in ms 41285 var CHECK_BUFFER_DELAY = 500; 41286 41287 /** 41288 * Determines if we should call endOfStream on the media source based 41289 * on the state of the buffer or if appened segment was the final 41290 * segment in the playlist. 41291 * 41292 * @param {Object} playlist a media playlist object 41293 * @param {Object} mediaSource the MediaSource object 41294 * @param {Number} segmentIndex the index of segment we last appended 41295 * @returns {Boolean} do we need to call endOfStream on the MediaSource 41296 */ 41297 var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) { 41298 if (!playlist || !mediaSource) { 41299 return false; 41300 } 41301 41302 var segments = playlist.segments; 41303 41304 // determine a few boolean values to help make the branch below easier 41305 // to read 41306 var appendedLastSegment = segmentIndex === segments.length; 41307 41308 // if we've buffered to the end of the video, we need to call endOfStream 41309 // so that MediaSources can trigger the `ended` event when it runs out of 41310 // buffered data instead of waiting for me 41311 return playlist.endList && mediaSource.readyState === 'open' && appendedLastSegment; 41312 }; 41313 41314 var finite = function finite(num) { 41315 return typeof num === 'number' && isFinite(num); 41316 }; 41317 41318 var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) { 41319 // Although these checks should most likely cover non 'main' types, for now it narrows 41320 // the scope of our checks. 41321 if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) { 41322 return null; 41323 } 41324 41325 if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) { 41326 return 'Neither audio nor video found in segment.'; 41327 } 41328 41329 if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) { 41330 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.'; 41331 } 41332 41333 if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) { 41334 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.'; 41335 } 41336 41337 return null; 41338 }; 41339 41340 /** 41341 * Calculates a time value that is safe to remove from the back buffer without interupting 41342 * playback. 41343 * 41344 * @param {TimeRange} seekable 41345 * The current seekable range 41346 * @param {Number} currentTime 41347 * The current time of the player 41348 * @param {Number} targetDuration 41349 * The target duration of the current playlist 41350 * @return {Number} 41351 * Time that is safe to remove from the back buffer without interupting playback 41352 */ 41353 var safeBackBufferTrimTime = function safeBackBufferTrimTime(seekable$$1, currentTime, targetDuration) { 41354 var removeToTime = void 0; 41355 41356 if (seekable$$1.length && seekable$$1.start(0) > 0 && seekable$$1.start(0) < currentTime) { 41357 // If we have a seekable range use that as the limit for what can be removed safely 41358 removeToTime = seekable$$1.start(0); 41359 } else { 41360 // otherwise remove anything older than 30 seconds before the current play head 41361 removeToTime = currentTime - 30; 41362 } 41363 41364 // Don't allow removing from the buffer within target duration of current time 41365 // to avoid the possibility of removing the GOP currently being played which could 41366 // cause playback stalls. 41367 return Math.min(removeToTime, currentTime - targetDuration); 41368 }; 41369 41370 var segmentInfoString = function segmentInfoString(segmentInfo) { 41371 var _segmentInfo$segment = segmentInfo.segment, 41372 start = _segmentInfo$segment.start, 41373 end = _segmentInfo$segment.end, 41374 _segmentInfo$playlist = segmentInfo.playlist, 41375 seq = _segmentInfo$playlist.mediaSequence, 41376 id = _segmentInfo$playlist.id, 41377 _segmentInfo$playlist2 = _segmentInfo$playlist.segments, 41378 segments = _segmentInfo$playlist2 === undefined ? [] : _segmentInfo$playlist2, 41379 index = segmentInfo.mediaIndex, 41380 timeline = segmentInfo.timeline; 41381 41382 return ['appending [' + index + '] of [' + seq + ', ' + (seq + segments.length) + '] from playlist [' + id + ']', '[' + start + ' => ' + end + '] in timeline [' + timeline + ']'].join(' '); 41383 }; 41384 41385 /** 41386 * An object that manages segment loading and appending. 41387 * 41388 * @class SegmentLoader 41389 * @param {Object} options required and optional options 41390 * @extends videojs.EventTarget 41391 */ 41392 41393 var SegmentLoader = function (_videojs$EventTarget) { 41394 inherits$3(SegmentLoader, _videojs$EventTarget); 41395 41396 function SegmentLoader(settings) { 41397 classCallCheck$3(this, SegmentLoader); 41398 41399 // check pre-conditions 41400 var _this = possibleConstructorReturn$3(this, (SegmentLoader.__proto__ || Object.getPrototypeOf(SegmentLoader)).call(this)); 41401 41402 if (!settings) { 41403 throw new TypeError('Initialization settings are required'); 41404 } 41405 if (typeof settings.currentTime !== 'function') { 41406 throw new TypeError('No currentTime getter specified'); 41407 } 41408 if (!settings.mediaSource) { 41409 throw new TypeError('No MediaSource specified'); 41410 } 41411 // public properties 41412 _this.bandwidth = settings.bandwidth; 41413 _this.throughput = { rate: 0, count: 0 }; 41414 _this.roundTrip = NaN; 41415 _this.resetStats_(); 41416 _this.mediaIndex = null; 41417 41418 // private settings 41419 _this.hasPlayed_ = settings.hasPlayed; 41420 _this.currentTime_ = settings.currentTime; 41421 _this.seekable_ = settings.seekable; 41422 _this.seeking_ = settings.seeking; 41423 _this.duration_ = settings.duration; 41424 _this.mediaSource_ = settings.mediaSource; 41425 _this.hls_ = settings.hls; 41426 _this.loaderType_ = settings.loaderType; 41427 _this.startingMedia_ = void 0; 41428 _this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 41429 _this.goalBufferLength_ = settings.goalBufferLength; 41430 _this.sourceType_ = settings.sourceType; 41431 _this.state_ = 'INIT'; 41432 41433 // private instance variables 41434 _this.checkBufferTimeout_ = null; 41435 _this.error_ = void 0; 41436 _this.currentTimeline_ = -1; 41437 _this.pendingSegment_ = null; 41438 _this.mimeType_ = null; 41439 _this.sourceUpdater_ = null; 41440 _this.xhrOptions_ = null; 41441 41442 // Fragmented mp4 playback 41443 _this.activeInitSegmentId_ = null; 41444 _this.initSegments_ = {}; 41445 41446 _this.decrypter_ = settings.decrypter; 41447 41448 // Manages the tracking and generation of sync-points, mappings 41449 // between a time in the display time and a segment index within 41450 // a playlist 41451 _this.syncController_ = settings.syncController; 41452 _this.syncPoint_ = { 41453 segmentIndex: 0, 41454 time: 0 41455 }; 41456 41457 _this.syncController_.on('syncinfoupdate', function () { 41458 return _this.trigger('syncinfoupdate'); 41459 }); 41460 41461 _this.mediaSource_.addEventListener('sourceopen', function () { 41462 return _this.ended_ = false; 41463 }); 41464 41465 // ...for determining the fetch location 41466 _this.fetchAtBuffer_ = false; 41467 41468 _this.logger_ = logger('SegmentLoader[' + _this.loaderType_ + ']'); 41469 41470 Object.defineProperty(_this, 'state', { 41471 get: function get$$1() { 41472 return this.state_; 41473 }, 41474 set: function set$$1(newState) { 41475 if (newState !== this.state_) { 41476 this.logger_(this.state_ + ' -> ' + newState); 41477 this.state_ = newState; 41478 } 41479 } 41480 }); 41481 return _this; 41482 } 41483 41484 /** 41485 * reset all of our media stats 41486 * 41487 * @private 41488 */ 41489 41490 createClass$2(SegmentLoader, [{ 41491 key: 'resetStats_', 41492 value: function resetStats_() { 41493 this.mediaBytesTransferred = 0; 41494 this.mediaRequests = 0; 41495 this.mediaRequestsAborted = 0; 41496 this.mediaRequestsTimedout = 0; 41497 this.mediaRequestsErrored = 0; 41498 this.mediaTransferDuration = 0; 41499 this.mediaSecondsLoaded = 0; 41500 } 41501 41502 /** 41503 * dispose of the SegmentLoader and reset to the default state 41504 */ 41505 41506 }, { 41507 key: 'dispose', 41508 value: function dispose() { 41509 this.state = 'DISPOSED'; 41510 this.pause(); 41511 this.abort_(); 41512 if (this.sourceUpdater_) { 41513 this.sourceUpdater_.dispose(); 41514 } 41515 this.resetStats_(); 41516 } 41517 41518 /** 41519 * abort anything that is currently doing on with the SegmentLoader 41520 * and reset to a default state 41521 */ 41522 41523 }, { 41524 key: 'abort', 41525 value: function abort() { 41526 if (this.state !== 'WAITING') { 41527 if (this.pendingSegment_) { 41528 this.pendingSegment_ = null; 41529 } 41530 return; 41531 } 41532 41533 this.abort_(); 41534 41535 // We aborted the requests we were waiting on, so reset the loader's state to READY 41536 // since we are no longer "waiting" on any requests. XHR callback is not always run 41537 // when the request is aborted. This will prevent the loader from being stuck in the 41538 // WAITING state indefinitely. 41539 this.state = 'READY'; 41540 41541 // don't wait for buffer check timeouts to begin fetching the 41542 // next segment 41543 if (!this.paused()) { 41544 this.monitorBuffer_(); 41545 } 41546 } 41547 41548 /** 41549 * abort all pending xhr requests and null any pending segements 41550 * 41551 * @private 41552 */ 41553 41554 }, { 41555 key: 'abort_', 41556 value: function abort_() { 41557 if (this.pendingSegment_) { 41558 this.pendingSegment_.abortRequests(); 41559 } 41560 41561 // clear out the segment being processed 41562 this.pendingSegment_ = null; 41563 } 41564 41565 /** 41566 * set an error on the segment loader and null out any pending segements 41567 * 41568 * @param {Error} error the error to set on the SegmentLoader 41569 * @return {Error} the error that was set or that is currently set 41570 */ 41571 41572 }, { 41573 key: 'error', 41574 value: function error(_error) { 41575 if (typeof _error !== 'undefined') { 41576 this.error_ = _error; 41577 } 41578 41579 this.pendingSegment_ = null; 41580 return this.error_; 41581 } 41582 }, { 41583 key: 'endOfStream', 41584 value: function endOfStream() { 41585 this.ended_ = true; 41586 this.pause(); 41587 this.trigger('ended'); 41588 } 41589 41590 /** 41591 * Indicates which time ranges are buffered 41592 * 41593 * @return {TimeRange} 41594 * TimeRange object representing the current buffered ranges 41595 */ 41596 41597 }, { 41598 key: 'buffered_', 41599 value: function buffered_() { 41600 if (!this.sourceUpdater_) { 41601 return videojs$1.createTimeRanges(); 41602 } 41603 41604 return this.sourceUpdater_.buffered(); 41605 } 41606 41607 /** 41608 * Gets and sets init segment for the provided map 41609 * 41610 * @param {Object} map 41611 * The map object representing the init segment to get or set 41612 * @param {Boolean=} set 41613 * If true, the init segment for the provided map should be saved 41614 * @return {Object} 41615 * map object for desired init segment 41616 */ 41617 41618 }, { 41619 key: 'initSegment', 41620 value: function initSegment(map) { 41621 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 41622 41623 if (!map) { 41624 return null; 41625 } 41626 41627 var id = initSegmentId(map); 41628 var storedMap = this.initSegments_[id]; 41629 41630 if (set$$1 && !storedMap && map.bytes) { 41631 this.initSegments_[id] = storedMap = { 41632 resolvedUri: map.resolvedUri, 41633 byterange: map.byterange, 41634 bytes: map.bytes 41635 }; 41636 } 41637 41638 return storedMap || map; 41639 } 41640 41641 /** 41642 * Returns true if all configuration required for loading is present, otherwise false. 41643 * 41644 * @return {Boolean} True if the all configuration is ready for loading 41645 * @private 41646 */ 41647 41648 }, { 41649 key: 'couldBeginLoading_', 41650 value: function couldBeginLoading_() { 41651 return this.playlist_ && ( 41652 // the source updater is created when init_ is called, so either having a 41653 // source updater or being in the INIT state with a mimeType is enough 41654 // to say we have all the needed configuration to start loading. 41655 this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused(); 41656 } 41657 41658 /** 41659 * load a playlist and start to fill the buffer 41660 */ 41661 41662 }, { 41663 key: 'load', 41664 value: function load() { 41665 // un-pause 41666 this.monitorBuffer_(); 41667 41668 // if we don't have a playlist yet, keep waiting for one to be 41669 // specified 41670 if (!this.playlist_) { 41671 return; 41672 } 41673 41674 // not sure if this is the best place for this 41675 this.syncController_.setDateTimeMapping(this.playlist_); 41676 41677 // if all the configuration is ready, initialize and begin loading 41678 if (this.state === 'INIT' && this.couldBeginLoading_()) { 41679 return this.init_(); 41680 } 41681 41682 // if we're in the middle of processing a segment already, don't 41683 // kick off an additional segment request 41684 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') { 41685 return; 41686 } 41687 41688 this.state = 'READY'; 41689 } 41690 41691 /** 41692 * Once all the starting parameters have been specified, begin 41693 * operation. This method should only be invoked from the INIT 41694 * state. 41695 * 41696 * @private 41697 */ 41698 41699 }, { 41700 key: 'init_', 41701 value: function init_() { 41702 this.state = 'READY'; 41703 this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_, this.loaderType_, this.sourceBufferEmitter_); 41704 this.resetEverything(); 41705 return this.monitorBuffer_(); 41706 } 41707 41708 /** 41709 * set a playlist on the segment loader 41710 * 41711 * @param {PlaylistLoader} media the playlist to set on the segment loader 41712 */ 41713 41714 }, { 41715 key: 'playlist', 41716 value: function playlist(newPlaylist) { 41717 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 41718 41719 if (!newPlaylist) { 41720 return; 41721 } 41722 41723 var oldPlaylist = this.playlist_; 41724 var segmentInfo = this.pendingSegment_; 41725 41726 this.playlist_ = newPlaylist; 41727 this.xhrOptions_ = options; 41728 41729 // when we haven't started playing yet, the start of a live playlist 41730 // is always our zero-time so force a sync update each time the playlist 41731 // is refreshed from the server 41732 if (!this.hasPlayed_()) { 41733 newPlaylist.syncInfo = { 41734 mediaSequence: newPlaylist.mediaSequence, 41735 time: 0 41736 }; 41737 } 41738 41739 var oldId = oldPlaylist ? oldPlaylist.id : null; 41740 41741 this.logger_('playlist update [' + oldId + ' => ' + newPlaylist.id + ']'); 41742 41743 // in VOD, this is always a rendition switch (or we updated our syncInfo above) 41744 // in LIVE, we always want to update with new playlists (including refreshes) 41745 this.trigger('syncinfoupdate'); 41746 41747 // if we were unpaused but waiting for a playlist, start 41748 // buffering now 41749 if (this.state === 'INIT' && this.couldBeginLoading_()) { 41750 return this.init_(); 41751 } 41752 41753 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 41754 if (this.mediaIndex !== null) { 41755 // we must "resync" the segment loader when we switch renditions and 41756 // the segment loader is already synced to the previous rendition 41757 this.resyncLoader(); 41758 } 41759 41760 // the rest of this function depends on `oldPlaylist` being defined 41761 return; 41762 } 41763 41764 // we reloaded the same playlist so we are in a live scenario 41765 // and we will likely need to adjust the mediaIndex 41766 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 41767 41768 this.logger_('live window shift [' + mediaSequenceDiff + ']'); 41769 41770 // update the mediaIndex on the SegmentLoader 41771 // this is important because we can abort a request and this value must be 41772 // equal to the last appended mediaIndex 41773 if (this.mediaIndex !== null) { 41774 this.mediaIndex -= mediaSequenceDiff; 41775 } 41776 41777 // update the mediaIndex on the SegmentInfo object 41778 // this is important because we will update this.mediaIndex with this value 41779 // in `handleUpdateEnd_` after the segment has been successfully appended 41780 if (segmentInfo) { 41781 segmentInfo.mediaIndex -= mediaSequenceDiff; 41782 41783 // we need to update the referenced segment so that timing information is 41784 // saved for the new playlist's segment, however, if the segment fell off the 41785 // playlist, we can leave the old reference and just lose the timing info 41786 if (segmentInfo.mediaIndex >= 0) { 41787 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 41788 } 41789 } 41790 41791 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 41792 } 41793 41794 /** 41795 * Prevent the loader from fetching additional segments. If there 41796 * is a segment request outstanding, it will finish processing 41797 * before the loader halts. A segment loader can be unpaused by 41798 * calling load(). 41799 */ 41800 41801 }, { 41802 key: 'pause', 41803 value: function pause() { 41804 if (this.checkBufferTimeout_) { 41805 window_1.clearTimeout(this.checkBufferTimeout_); 41806 41807 this.checkBufferTimeout_ = null; 41808 } 41809 } 41810 41811 /** 41812 * Returns whether the segment loader is fetching additional 41813 * segments when given the opportunity. This property can be 41814 * modified through calls to pause() and load(). 41815 */ 41816 41817 }, { 41818 key: 'paused', 41819 value: function paused() { 41820 return this.checkBufferTimeout_ === null; 41821 } 41822 41823 /** 41824 * create/set the following mimetype on the SourceBuffer through a 41825 * SourceUpdater 41826 * 41827 * @param {String} mimeType the mime type string to use 41828 * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer 41829 * is added to the media source 41830 */ 41831 41832 }, { 41833 key: 'mimeType', 41834 value: function mimeType(_mimeType, sourceBufferEmitter) { 41835 if (this.mimeType_) { 41836 return; 41837 } 41838 41839 this.mimeType_ = _mimeType; 41840 this.sourceBufferEmitter_ = sourceBufferEmitter; 41841 // if we were unpaused but waiting for a sourceUpdater, start 41842 // buffering now 41843 if (this.state === 'INIT' && this.couldBeginLoading_()) { 41844 this.init_(); 41845 } 41846 } 41847 41848 /** 41849 * Delete all the buffered data and reset the SegmentLoader 41850 */ 41851 41852 }, { 41853 key: 'resetEverything', 41854 value: function resetEverything() { 41855 this.ended_ = false; 41856 this.resetLoader(); 41857 this.remove(0, this.duration_()); 41858 this.trigger('reseteverything'); 41859 } 41860 41861 /** 41862 * Force the SegmentLoader to resync and start loading around the currentTime instead 41863 * of starting at the end of the buffer 41864 * 41865 * Useful for fast quality changes 41866 */ 41867 41868 }, { 41869 key: 'resetLoader', 41870 value: function resetLoader() { 41871 this.fetchAtBuffer_ = false; 41872 this.resyncLoader(); 41873 } 41874 41875 /** 41876 * Force the SegmentLoader to restart synchronization and make a conservative guess 41877 * before returning to the simple walk-forward method 41878 */ 41879 41880 }, { 41881 key: 'resyncLoader', 41882 value: function resyncLoader() { 41883 this.mediaIndex = null; 41884 this.syncPoint_ = null; 41885 this.abort(); 41886 } 41887 41888 /** 41889 * Remove any data in the source buffer between start and end times 41890 * @param {Number} start - the start time of the region to remove from the buffer 41891 * @param {Number} end - the end time of the region to remove from the buffer 41892 */ 41893 41894 }, { 41895 key: 'remove', 41896 value: function remove(start, end) { 41897 if (this.sourceUpdater_) { 41898 this.sourceUpdater_.remove(start, end); 41899 } 41900 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 41901 } 41902 41903 /** 41904 * (re-)schedule monitorBufferTick_ to run as soon as possible 41905 * 41906 * @private 41907 */ 41908 41909 }, { 41910 key: 'monitorBuffer_', 41911 value: function monitorBuffer_() { 41912 if (this.checkBufferTimeout_) { 41913 window_1.clearTimeout(this.checkBufferTimeout_); 41914 } 41915 41916 this.checkBufferTimeout_ = window_1.setTimeout(this.monitorBufferTick_.bind(this), 1); 41917 } 41918 41919 /** 41920 * As long as the SegmentLoader is in the READY state, periodically 41921 * invoke fillBuffer_(). 41922 * 41923 * @private 41924 */ 41925 41926 }, { 41927 key: 'monitorBufferTick_', 41928 value: function monitorBufferTick_() { 41929 if (this.state === 'READY') { 41930 this.fillBuffer_(); 41931 } 41932 41933 if (this.checkBufferTimeout_) { 41934 window_1.clearTimeout(this.checkBufferTimeout_); 41935 } 41936 41937 this.checkBufferTimeout_ = window_1.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY); 41938 } 41939 41940 /** 41941 * fill the buffer with segements unless the sourceBuffers are 41942 * currently updating 41943 * 41944 * Note: this function should only ever be called by monitorBuffer_ 41945 * and never directly 41946 * 41947 * @private 41948 */ 41949 41950 }, { 41951 key: 'fillBuffer_', 41952 value: function fillBuffer_() { 41953 if (this.sourceUpdater_.updating()) { 41954 return; 41955 } 41956 41957 if (!this.syncPoint_) { 41958 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_()); 41959 } 41960 41961 // see if we need to begin loading immediately 41962 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_); 41963 41964 if (!segmentInfo) { 41965 return; 41966 } 41967 41968 var isEndOfStream = detectEndOfStream(this.playlist_, this.mediaSource_, segmentInfo.mediaIndex); 41969 41970 if (isEndOfStream) { 41971 this.endOfStream(); 41972 return; 41973 } 41974 41975 if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 && this.mediaSource_.readyState === 'ended' && !this.seeking_()) { 41976 return; 41977 } 41978 41979 // We will need to change timestampOffset of the sourceBuffer if either of 41980 // the following conditions are true: 41981 // - The segment.timeline !== this.currentTimeline 41982 // (we are crossing a discontinuity somehow) 41983 // - The "timestampOffset" for the start of this segment is less than 41984 // the currently set timestampOffset 41985 if (segmentInfo.timeline !== this.currentTimeline_ || segmentInfo.startOfSegment !== null && segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset()) { 41986 this.syncController_.reset(); 41987 segmentInfo.timestampOffset = segmentInfo.startOfSegment; 41988 } 41989 41990 this.loadSegment_(segmentInfo); 41991 } 41992 41993 /** 41994 * Determines what segment request should be made, given current playback 41995 * state. 41996 * 41997 * @param {TimeRanges} buffered - the state of the buffer 41998 * @param {Object} playlist - the playlist object to fetch segments from 41999 * @param {Number} mediaIndex - the previous mediaIndex fetched or null 42000 * @param {Boolean} hasPlayed - a flag indicating whether we have played or not 42001 * @param {Number} currentTime - the playback position in seconds 42002 * @param {Object} syncPoint - a segment info object that describes the 42003 * @returns {Object} a segment request object that describes the segment to load 42004 */ 42005 42006 }, { 42007 key: 'checkBuffer_', 42008 value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) { 42009 var lastBufferedEnd = 0; 42010 var startOfSegment = void 0; 42011 42012 if (buffered.length) { 42013 lastBufferedEnd = buffered.end(buffered.length - 1); 42014 } 42015 42016 var bufferedTime = Math.max(0, lastBufferedEnd - currentTime); 42017 42018 if (!playlist.segments.length) { 42019 return null; 42020 } 42021 42022 // if there is plenty of content buffered, and the video has 42023 // been played before relax for awhile 42024 if (bufferedTime >= this.goalBufferLength_()) { 42025 return null; 42026 } 42027 42028 // if the video has not yet played once, and we already have 42029 // one segment downloaded do nothing 42030 if (!hasPlayed && bufferedTime >= 1) { 42031 return null; 42032 } 42033 42034 // When the syncPoint is null, there is no way of determining a good 42035 // conservative segment index to fetch from 42036 // The best thing to do here is to get the kind of sync-point data by 42037 // making a request 42038 if (syncPoint === null) { 42039 mediaIndex = this.getSyncSegmentCandidate_(playlist); 42040 return this.generateSegmentInfo_(playlist, mediaIndex, null, true); 42041 } 42042 42043 // Under normal playback conditions fetching is a simple walk forward 42044 if (mediaIndex !== null) { 42045 var segment = playlist.segments[mediaIndex]; 42046 42047 if (segment && segment.end) { 42048 startOfSegment = segment.end; 42049 } else { 42050 startOfSegment = lastBufferedEnd; 42051 } 42052 return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false); 42053 } 42054 42055 // There is a sync-point but the lack of a mediaIndex indicates that 42056 // we need to make a good conservative guess about which segment to 42057 // fetch 42058 if (this.fetchAtBuffer_) { 42059 // Find the segment containing the end of the buffer 42060 var mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time); 42061 42062 mediaIndex = mediaSourceInfo.mediaIndex; 42063 startOfSegment = mediaSourceInfo.startTime; 42064 } else { 42065 // Find the segment containing currentTime 42066 var _mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time); 42067 42068 mediaIndex = _mediaSourceInfo.mediaIndex; 42069 startOfSegment = _mediaSourceInfo.startTime; 42070 } 42071 42072 return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false); 42073 } 42074 42075 /** 42076 * The segment loader has no recourse except to fetch a segment in the 42077 * current playlist and use the internal timestamps in that segment to 42078 * generate a syncPoint. This function returns a good candidate index 42079 * for that process. 42080 * 42081 * @param {Object} playlist - the playlist object to look for a 42082 * @returns {Number} An index of a segment from the playlist to load 42083 */ 42084 42085 }, { 42086 key: 'getSyncSegmentCandidate_', 42087 value: function getSyncSegmentCandidate_(playlist) { 42088 var _this2 = this; 42089 42090 if (this.currentTimeline_ === -1) { 42091 return 0; 42092 } 42093 42094 var segmentIndexArray = playlist.segments.map(function (s, i) { 42095 return { 42096 timeline: s.timeline, 42097 segmentIndex: i 42098 }; 42099 }).filter(function (s) { 42100 return s.timeline === _this2.currentTimeline_; 42101 }); 42102 42103 if (segmentIndexArray.length) { 42104 return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex; 42105 } 42106 42107 return Math.max(playlist.segments.length - 1, 0); 42108 } 42109 }, { 42110 key: 'generateSegmentInfo_', 42111 value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) { 42112 if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) { 42113 return null; 42114 } 42115 42116 var segment = playlist.segments[mediaIndex]; 42117 42118 return { 42119 requestId: 'segment-loader-' + Math.random(), 42120 // resolve the segment URL relative to the playlist 42121 uri: segment.resolvedUri, 42122 // the segment's mediaIndex at the time it was requested 42123 mediaIndex: mediaIndex, 42124 // whether or not to update the SegmentLoader's state with this 42125 // segment's mediaIndex 42126 isSyncRequest: isSyncRequest, 42127 startOfSegment: startOfSegment, 42128 // the segment's playlist 42129 playlist: playlist, 42130 // unencrypted bytes of the segment 42131 bytes: null, 42132 // when a key is defined for this segment, the encrypted bytes 42133 encryptedBytes: null, 42134 // The target timestampOffset for this segment when we append it 42135 // to the source buffer 42136 timestampOffset: null, 42137 // The timeline that the segment is in 42138 timeline: segment.timeline, 42139 // The expected duration of the segment in seconds 42140 duration: segment.duration, 42141 // retain the segment in case the playlist updates while doing an async process 42142 segment: segment 42143 }; 42144 } 42145 42146 /** 42147 * Determines if the network has enough bandwidth to complete the current segment 42148 * request in a timely manner. If not, the request will be aborted early and bandwidth 42149 * updated to trigger a playlist switch. 42150 * 42151 * @param {Object} stats 42152 * Object containing stats about the request timing and size 42153 * @return {Boolean} True if the request was aborted, false otherwise 42154 * @private 42155 */ 42156 42157 }, { 42158 key: 'abortRequestEarly_', 42159 value: function abortRequestEarly_(stats) { 42160 if (this.hls_.tech_.paused() || 42161 // Don't abort if the current playlist is on the lowestEnabledRendition 42162 // TODO: Replace using timeout with a boolean indicating whether this playlist is 42163 // the lowestEnabledRendition. 42164 !this.xhrOptions_.timeout || 42165 // Don't abort if we have no bandwidth information to estimate segment sizes 42166 !this.playlist_.attributes.BANDWIDTH) { 42167 return false; 42168 } 42169 42170 // Wait at least 1 second since the first byte of data has been received before 42171 // using the calculated bandwidth from the progress event to allow the bitrate 42172 // to stabilize 42173 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 42174 return false; 42175 } 42176 42177 var currentTime = this.currentTime_(); 42178 var measuredBandwidth = stats.bandwidth; 42179 var segmentDuration = this.pendingSegment_.duration; 42180 42181 var requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); 42182 42183 // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 42184 // if we are only left with less than 1 second when the request completes. 42185 // A negative timeUntilRebuffering indicates we are already rebuffering 42186 var timeUntilRebuffer$$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1; 42187 42188 // Only consider aborting early if the estimated time to finish the download 42189 // is larger than the estimated time until the player runs out of forward buffer 42190 if (requestTimeRemaining <= timeUntilRebuffer$$1) { 42191 return false; 42192 } 42193 42194 var switchCandidate = minRebufferMaxBandwidthSelector({ 42195 master: this.hls_.playlists.master, 42196 currentTime: currentTime, 42197 bandwidth: measuredBandwidth, 42198 duration: this.duration_(), 42199 segmentDuration: segmentDuration, 42200 timeUntilRebuffer: timeUntilRebuffer$$1, 42201 currentTimeline: this.currentTimeline_, 42202 syncController: this.syncController_ 42203 }); 42204 42205 if (!switchCandidate) { 42206 return; 42207 } 42208 42209 var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$$1; 42210 42211 var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 42212 42213 var minimumTimeSaving = 0.5; 42214 42215 // If we are already rebuffering, increase the amount of variance we add to the 42216 // potential round trip time of the new request so that we are not too aggressive 42217 // with switching to a playlist that might save us a fraction of a second. 42218 if (timeUntilRebuffer$$1 <= TIME_FUDGE_FACTOR) { 42219 minimumTimeSaving = 1; 42220 } 42221 42222 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) { 42223 return false; 42224 } 42225 42226 // set the bandwidth to that of the desired playlist being sure to scale by 42227 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 42228 // don't trigger a bandwidthupdate as the bandwidth is artifial 42229 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 42230 this.abort(); 42231 this.trigger('earlyabort'); 42232 return true; 42233 } 42234 42235 /** 42236 * XHR `progress` event handler 42237 * 42238 * @param {Event} 42239 * The XHR `progress` event 42240 * @param {Object} simpleSegment 42241 * A simplified segment object copy 42242 * @private 42243 */ 42244 42245 }, { 42246 key: 'handleProgress_', 42247 value: function handleProgress_(event, simpleSegment) { 42248 if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) { 42249 return; 42250 } 42251 42252 this.trigger('progress'); 42253 } 42254 42255 /** 42256 * load a specific segment from a request into the buffer 42257 * 42258 * @private 42259 */ 42260 42261 }, { 42262 key: 'loadSegment_', 42263 value: function loadSegment_(segmentInfo) { 42264 this.state = 'WAITING'; 42265 this.pendingSegment_ = segmentInfo; 42266 this.trimBackBuffer_(segmentInfo); 42267 42268 segmentInfo.abortRequests = mediaSegmentRequest(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.createSimplifiedSegmentObj_(segmentInfo), 42269 // progress callback 42270 this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this)); 42271 } 42272 42273 /** 42274 * trim the back buffer so that we don't have too much data 42275 * in the source buffer 42276 * 42277 * @private 42278 * 42279 * @param {Object} segmentInfo - the current segment 42280 */ 42281 42282 }, { 42283 key: 'trimBackBuffer_', 42284 value: function trimBackBuffer_(segmentInfo) { 42285 var removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); 42286 42287 // Chrome has a hard limit of 150MB of 42288 // buffer and a very conservative "garbage collector" 42289 // We manually clear out the old buffer to ensure 42290 // we don't trigger the QuotaExceeded error 42291 // on the source buffer during subsequent appends 42292 42293 if (removeToTime > 0) { 42294 this.remove(0, removeToTime); 42295 } 42296 } 42297 42298 /** 42299 * created a simplified copy of the segment object with just the 42300 * information necessary to perform the XHR and decryption 42301 * 42302 * @private 42303 * 42304 * @param {Object} segmentInfo - the current segment 42305 * @returns {Object} a simplified segment object copy 42306 */ 42307 42308 }, { 42309 key: 'createSimplifiedSegmentObj_', 42310 value: function createSimplifiedSegmentObj_(segmentInfo) { 42311 var segment = segmentInfo.segment; 42312 var simpleSegment = { 42313 resolvedUri: segment.resolvedUri, 42314 byterange: segment.byterange, 42315 requestId: segmentInfo.requestId 42316 }; 42317 42318 if (segment.key) { 42319 // if the media sequence is greater than 2^32, the IV will be incorrect 42320 // assuming 10s segments, that would be about 1300 years 42321 var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 42322 42323 simpleSegment.key = { 42324 resolvedUri: segment.key.resolvedUri, 42325 iv: iv 42326 }; 42327 } 42328 42329 if (segment.map) { 42330 simpleSegment.map = this.initSegment(segment.map); 42331 } 42332 42333 return simpleSegment; 42334 } 42335 42336 /** 42337 * Handle the callback from the segmentRequest function and set the 42338 * associated SegmentLoader state and errors if necessary 42339 * 42340 * @private 42341 */ 42342 42343 }, { 42344 key: 'segmentRequestFinished_', 42345 value: function segmentRequestFinished_(error, simpleSegment) { 42346 // every request counts as a media request even if it has been aborted 42347 // or canceled due to a timeout 42348 this.mediaRequests += 1; 42349 42350 if (simpleSegment.stats) { 42351 this.mediaBytesTransferred += simpleSegment.stats.bytesReceived; 42352 this.mediaTransferDuration += simpleSegment.stats.roundTripTime; 42353 } 42354 42355 // The request was aborted and the SegmentLoader has already been reset 42356 if (!this.pendingSegment_) { 42357 this.mediaRequestsAborted += 1; 42358 return; 42359 } 42360 42361 // the request was aborted and the SegmentLoader has already started 42362 // another request. this can happen when the timeout for an aborted 42363 // request triggers due to a limitation in the XHR library 42364 // do not count this as any sort of request or we risk double-counting 42365 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 42366 return; 42367 } 42368 42369 // an error occurred from the active pendingSegment_ so reset everything 42370 if (error) { 42371 this.pendingSegment_ = null; 42372 this.state = 'READY'; 42373 42374 // the requests were aborted just record the aborted stat and exit 42375 // this is not a true error condition and nothing corrective needs 42376 // to be done 42377 if (error.code === REQUEST_ERRORS.ABORTED) { 42378 this.mediaRequestsAborted += 1; 42379 return; 42380 } 42381 42382 this.pause(); 42383 42384 // the error is really just that at least one of the requests timed-out 42385 // set the bandwidth to a very low value and trigger an ABR switch to 42386 // take emergency action 42387 if (error.code === REQUEST_ERRORS.TIMEOUT) { 42388 this.mediaRequestsTimedout += 1; 42389 this.bandwidth = 1; 42390 this.roundTrip = NaN; 42391 this.trigger('bandwidthupdate'); 42392 return; 42393 } 42394 42395 // if control-flow has arrived here, then the error is real 42396 // emit an error event to blacklist the current playlist 42397 this.mediaRequestsErrored += 1; 42398 this.error(error); 42399 this.trigger('error'); 42400 return; 42401 } 42402 42403 // the response was a success so set any bandwidth stats the request 42404 // generated for ABR purposes 42405 this.bandwidth = simpleSegment.stats.bandwidth; 42406 this.roundTrip = simpleSegment.stats.roundTripTime; 42407 42408 // if this request included an initialization segment, save that data 42409 // to the initSegment cache 42410 if (simpleSegment.map) { 42411 simpleSegment.map = this.initSegment(simpleSegment.map, true); 42412 } 42413 42414 this.processSegmentResponse_(simpleSegment); 42415 } 42416 42417 /** 42418 * Move any important data from the simplified segment object 42419 * back to the real segment object for future phases 42420 * 42421 * @private 42422 */ 42423 42424 }, { 42425 key: 'processSegmentResponse_', 42426 value: function processSegmentResponse_(simpleSegment) { 42427 var segmentInfo = this.pendingSegment_; 42428 42429 segmentInfo.bytes = simpleSegment.bytes; 42430 if (simpleSegment.map) { 42431 segmentInfo.segment.map.bytes = simpleSegment.map.bytes; 42432 } 42433 42434 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; 42435 this.handleSegment_(); 42436 } 42437 42438 /** 42439 * append a decrypted segement to the SourceBuffer through a SourceUpdater 42440 * 42441 * @private 42442 */ 42443 42444 }, { 42445 key: 'handleSegment_', 42446 value: function handleSegment_() { 42447 var _this3 = this; 42448 42449 if (!this.pendingSegment_) { 42450 this.state = 'READY'; 42451 return; 42452 } 42453 42454 var segmentInfo = this.pendingSegment_; 42455 var segment = segmentInfo.segment; 42456 var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo); 42457 42458 // When we have our first timing info, determine what media types this loader is 42459 // dealing with. Although we're maintaining extra state, it helps to preserve the 42460 // separation of segment loader from the actual source buffers. 42461 if (typeof this.startingMedia_ === 'undefined' && timingInfo && ( 42462 // Guard against cases where we're not getting timing info at all until we are 42463 // certain that all streams will provide it. 42464 timingInfo.containsAudio || timingInfo.containsVideo)) { 42465 this.startingMedia_ = { 42466 containsAudio: timingInfo.containsAudio, 42467 containsVideo: timingInfo.containsVideo 42468 }; 42469 } 42470 42471 var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo); 42472 42473 if (illegalMediaSwitchError) { 42474 this.error({ 42475 message: illegalMediaSwitchError, 42476 blacklistDuration: Infinity 42477 }); 42478 this.trigger('error'); 42479 return; 42480 } 42481 42482 if (segmentInfo.isSyncRequest) { 42483 this.trigger('syncinfoupdate'); 42484 this.pendingSegment_ = null; 42485 this.state = 'READY'; 42486 return; 42487 } 42488 42489 if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) { 42490 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset); 42491 // fired when a timestamp offset is set in HLS (can also identify discontinuities) 42492 this.trigger('timestampoffset'); 42493 } 42494 42495 var timelineMapping = this.syncController_.mappingForTimeline(segmentInfo.timeline); 42496 42497 if (timelineMapping !== null) { 42498 this.trigger({ 42499 type: 'segmenttimemapping', 42500 mapping: timelineMapping 42501 }); 42502 } 42503 42504 this.state = 'APPENDING'; 42505 42506 // if the media initialization segment is changing, append it 42507 // before the content segment 42508 if (segment.map) { 42509 var initId = initSegmentId(segment.map); 42510 42511 if (!this.activeInitSegmentId_ || this.activeInitSegmentId_ !== initId) { 42512 var initSegment = this.initSegment(segment.map); 42513 42514 this.sourceUpdater_.appendBuffer(initSegment.bytes, function () { 42515 _this3.activeInitSegmentId_ = initId; 42516 }); 42517 } 42518 } 42519 42520 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 42521 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 42522 this.mediaSecondsLoaded += segment.end - segment.start; 42523 } else { 42524 this.mediaSecondsLoaded += segment.duration; 42525 } 42526 42527 this.logger_(segmentInfoString(segmentInfo)); 42528 42529 this.sourceUpdater_.appendBuffer(segmentInfo.bytes, this.handleUpdateEnd_.bind(this)); 42530 } 42531 42532 /** 42533 * callback to run when appendBuffer is finished. detects if we are 42534 * in a good state to do things with the data we got, or if we need 42535 * to wait for more 42536 * 42537 * @private 42538 */ 42539 42540 }, { 42541 key: 'handleUpdateEnd_', 42542 value: function handleUpdateEnd_() { 42543 if (!this.pendingSegment_) { 42544 this.state = 'READY'; 42545 if (!this.paused()) { 42546 this.monitorBuffer_(); 42547 } 42548 return; 42549 } 42550 42551 var segmentInfo = this.pendingSegment_; 42552 var segment = segmentInfo.segment; 42553 var isWalkingForward = this.mediaIndex !== null; 42554 42555 this.pendingSegment_ = null; 42556 this.recordThroughput_(segmentInfo); 42557 this.addSegmentMetadataCue_(segmentInfo); 42558 42559 this.state = 'READY'; 42560 42561 this.mediaIndex = segmentInfo.mediaIndex; 42562 this.fetchAtBuffer_ = true; 42563 this.currentTimeline_ = segmentInfo.timeline; 42564 42565 // We must update the syncinfo to recalculate the seekable range before 42566 // the following conditional otherwise it may consider this a bad "guess" 42567 // and attempt to resync when the post-update seekable window and live 42568 // point would mean that this was the perfect segment to fetch 42569 this.trigger('syncinfoupdate'); 42570 42571 // If we previously appended a segment that ends more than 3 targetDurations before 42572 // the currentTime_ that means that our conservative guess was too conservative. 42573 // In that case, reset the loader state so that we try to use any information gained 42574 // from the previous request to create a new, more accurate, sync-point. 42575 if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) { 42576 this.resetEverything(); 42577 return; 42578 } 42579 42580 // Don't do a rendition switch unless we have enough time to get a sync segment 42581 // and conservatively guess 42582 if (isWalkingForward) { 42583 this.trigger('bandwidthupdate'); 42584 } 42585 this.trigger('progress'); 42586 42587 // any time an update finishes and the last segment is in the 42588 // buffer, end the stream. this ensures the "ended" event will 42589 // fire if playback reaches that point. 42590 var isEndOfStream = detectEndOfStream(segmentInfo.playlist, this.mediaSource_, segmentInfo.mediaIndex + 1); 42591 42592 if (isEndOfStream) { 42593 this.endOfStream(); 42594 } 42595 42596 if (!this.paused()) { 42597 this.monitorBuffer_(); 42598 } 42599 } 42600 42601 /** 42602 * Records the current throughput of the decrypt, transmux, and append 42603 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 42604 * moving average of the throughput. `throughput.count` is the number of 42605 * data points in the average. 42606 * 42607 * @private 42608 * @param {Object} segmentInfo the object returned by loadSegment 42609 */ 42610 42611 }, { 42612 key: 'recordThroughput_', 42613 value: function recordThroughput_(segmentInfo) { 42614 var rate = this.throughput.rate; 42615 // Add one to the time to ensure that we don't accidentally attempt to divide 42616 // by zero in the case where the throughput is ridiculously high 42617 var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; 42618 // Multiply by 8000 to convert from bytes/millisecond to bits/second 42619 var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); 42620 42621 // This is just a cumulative moving average calculation: 42622 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 42623 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 42624 } 42625 42626 /** 42627 * Adds a cue to the segment-metadata track with some metadata information about the 42628 * segment 42629 * 42630 * @private 42631 * @param {Object} segmentInfo 42632 * the object returned by loadSegment 42633 * @method addSegmentMetadataCue_ 42634 */ 42635 42636 }, { 42637 key: 'addSegmentMetadataCue_', 42638 value: function addSegmentMetadataCue_(segmentInfo) { 42639 if (!this.segmentMetadataTrack_) { 42640 return; 42641 } 42642 42643 var segment = segmentInfo.segment; 42644 var start = segment.start; 42645 var end = segment.end; 42646 42647 // Do not try adding the cue if the start and end times are invalid. 42648 if (!finite(start) || !finite(end)) { 42649 return; 42650 } 42651 42652 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 42653 42654 var Cue = window_1.WebKitDataCue || window_1.VTTCue; 42655 var value = { 42656 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 42657 resolution: segmentInfo.playlist.attributes.RESOLUTION, 42658 codecs: segmentInfo.playlist.attributes.CODECS, 42659 byteLength: segmentInfo.byteLength, 42660 uri: segmentInfo.uri, 42661 timeline: segmentInfo.timeline, 42662 playlist: segmentInfo.playlist.uri, 42663 start: start, 42664 end: end 42665 }; 42666 var data = JSON.stringify(value); 42667 var cue = new Cue(start, end, data); 42668 42669 // Attach the metadata to the value property of the cue to keep consistency between 42670 // the differences of WebKitDataCue in safari and VTTCue in other browsers 42671 cue.value = value; 42672 42673 this.segmentMetadataTrack_.addCue(cue); 42674 } 42675 }]); 42676 return SegmentLoader; 42677 }(videojs$1.EventTarget); 42678 42679 /** 42680 * @file vtt-segment-loader.js 42681 */ 42682 42683 var VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(function (char) { 42684 return char.charCodeAt(0); 42685 })); 42686 42687 var uintToString = function uintToString(uintArray) { 42688 return String.fromCharCode.apply(null, uintArray); 42689 }; 42690 42691 /** 42692 * An object that manages segment loading and appending. 42693 * 42694 * @class VTTSegmentLoader 42695 * @param {Object} options required and optional options 42696 * @extends videojs.EventTarget 42697 */ 42698 42699 var VTTSegmentLoader = function (_SegmentLoader) { 42700 inherits$3(VTTSegmentLoader, _SegmentLoader); 42701 42702 function VTTSegmentLoader(settings) { 42703 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 42704 classCallCheck$3(this, VTTSegmentLoader); 42705 42706 // SegmentLoader requires a MediaSource be specified or it will throw an error; 42707 // however, VTTSegmentLoader has no need of a media source, so delete the reference 42708 var _this = possibleConstructorReturn$3(this, (VTTSegmentLoader.__proto__ || Object.getPrototypeOf(VTTSegmentLoader)).call(this, settings, options)); 42709 42710 _this.mediaSource_ = null; 42711 42712 _this.subtitlesTrack_ = null; 42713 return _this; 42714 } 42715 42716 /** 42717 * Indicates which time ranges are buffered 42718 * 42719 * @return {TimeRange} 42720 * TimeRange object representing the current buffered ranges 42721 */ 42722 42723 createClass$2(VTTSegmentLoader, [{ 42724 key: 'buffered_', 42725 value: function buffered_() { 42726 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) { 42727 return videojs$1.createTimeRanges(); 42728 } 42729 42730 var cues = this.subtitlesTrack_.cues; 42731 var start = cues[0].startTime; 42732 var end = cues[cues.length - 1].startTime; 42733 42734 return videojs$1.createTimeRanges([[start, end]]); 42735 } 42736 42737 /** 42738 * Gets and sets init segment for the provided map 42739 * 42740 * @param {Object} map 42741 * The map object representing the init segment to get or set 42742 * @param {Boolean=} set 42743 * If true, the init segment for the provided map should be saved 42744 * @return {Object} 42745 * map object for desired init segment 42746 */ 42747 42748 }, { 42749 key: 'initSegment', 42750 value: function initSegment(map) { 42751 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 42752 42753 if (!map) { 42754 return null; 42755 } 42756 42757 var id = initSegmentId(map); 42758 var storedMap = this.initSegments_[id]; 42759 42760 if (set$$1 && !storedMap && map.bytes) { 42761 // append WebVTT line terminators to the media initialization segment if it exists 42762 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 42763 // requires two or more WebVTT line terminators between the WebVTT header and the 42764 // rest of the file 42765 var combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 42766 var combinedSegment = new Uint8Array(combinedByteLength); 42767 42768 combinedSegment.set(map.bytes); 42769 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 42770 42771 this.initSegments_[id] = storedMap = { 42772 resolvedUri: map.resolvedUri, 42773 byterange: map.byterange, 42774 bytes: combinedSegment 42775 }; 42776 } 42777 42778 return storedMap || map; 42779 } 42780 42781 /** 42782 * Returns true if all configuration required for loading is present, otherwise false. 42783 * 42784 * @return {Boolean} True if the all configuration is ready for loading 42785 * @private 42786 */ 42787 42788 }, { 42789 key: 'couldBeginLoading_', 42790 value: function couldBeginLoading_() { 42791 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 42792 } 42793 42794 /** 42795 * Once all the starting parameters have been specified, begin 42796 * operation. This method should only be invoked from the INIT 42797 * state. 42798 * 42799 * @private 42800 */ 42801 42802 }, { 42803 key: 'init_', 42804 value: function init_() { 42805 this.state = 'READY'; 42806 this.resetEverything(); 42807 return this.monitorBuffer_(); 42808 } 42809 42810 /** 42811 * Set a subtitle track on the segment loader to add subtitles to 42812 * 42813 * @param {TextTrack=} track 42814 * The text track to add loaded subtitles to 42815 * @return {TextTrack} 42816 * Returns the subtitles track 42817 */ 42818 42819 }, { 42820 key: 'track', 42821 value: function track(_track) { 42822 if (typeof _track === 'undefined') { 42823 return this.subtitlesTrack_; 42824 } 42825 42826 this.subtitlesTrack_ = _track; 42827 42828 // if we were unpaused but waiting for a sourceUpdater, start 42829 // buffering now 42830 if (this.state === 'INIT' && this.couldBeginLoading_()) { 42831 this.init_(); 42832 } 42833 42834 return this.subtitlesTrack_; 42835 } 42836 42837 /** 42838 * Remove any data in the source buffer between start and end times 42839 * @param {Number} start - the start time of the region to remove from the buffer 42840 * @param {Number} end - the end time of the region to remove from the buffer 42841 */ 42842 42843 }, { 42844 key: 'remove', 42845 value: function remove(start, end) { 42846 removeCuesFromTrack(start, end, this.subtitlesTrack_); 42847 } 42848 42849 /** 42850 * fill the buffer with segements unless the sourceBuffers are 42851 * currently updating 42852 * 42853 * Note: this function should only ever be called by monitorBuffer_ 42854 * and never directly 42855 * 42856 * @private 42857 */ 42858 42859 }, { 42860 key: 'fillBuffer_', 42861 value: function fillBuffer_() { 42862 var _this2 = this; 42863 42864 if (!this.syncPoint_) { 42865 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_()); 42866 } 42867 42868 // see if we need to begin loading immediately 42869 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_); 42870 42871 segmentInfo = this.skipEmptySegments_(segmentInfo); 42872 42873 if (!segmentInfo) { 42874 return; 42875 } 42876 42877 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 42878 // We don't have the timestamp offset that we need to sync subtitles. 42879 // Rerun on a timestamp offset or user interaction. 42880 var checkTimestampOffset = function checkTimestampOffset() { 42881 _this2.state = 'READY'; 42882 if (!_this2.paused()) { 42883 // if not paused, queue a buffer check as soon as possible 42884 _this2.monitorBuffer_(); 42885 } 42886 }; 42887 42888 this.syncController_.one('timestampoffset', checkTimestampOffset); 42889 this.state = 'WAITING_ON_TIMELINE'; 42890 return; 42891 } 42892 42893 this.loadSegment_(segmentInfo); 42894 } 42895 42896 /** 42897 * Prevents the segment loader from requesting segments we know contain no subtitles 42898 * by walking forward until we find the next segment that we don't know whether it is 42899 * empty or not. 42900 * 42901 * @param {Object} segmentInfo 42902 * a segment info object that describes the current segment 42903 * @return {Object} 42904 * a segment info object that describes the current segment 42905 */ 42906 42907 }, { 42908 key: 'skipEmptySegments_', 42909 value: function skipEmptySegments_(segmentInfo) { 42910 while (segmentInfo && segmentInfo.segment.empty) { 42911 segmentInfo = this.generateSegmentInfo_(segmentInfo.playlist, segmentInfo.mediaIndex + 1, segmentInfo.startOfSegment + segmentInfo.duration, segmentInfo.isSyncRequest); 42912 } 42913 return segmentInfo; 42914 } 42915 42916 /** 42917 * append a decrypted segement to the SourceBuffer through a SourceUpdater 42918 * 42919 * @private 42920 */ 42921 42922 }, { 42923 key: 'handleSegment_', 42924 value: function handleSegment_() { 42925 var _this3 = this; 42926 42927 if (!this.pendingSegment_ || !this.subtitlesTrack_) { 42928 this.state = 'READY'; 42929 return; 42930 } 42931 42932 this.state = 'APPENDING'; 42933 42934 var segmentInfo = this.pendingSegment_; 42935 var segment = segmentInfo.segment; 42936 42937 // Make sure that vttjs has loaded, otherwise, wait till it finished loading 42938 if (typeof window_1.WebVTT !== 'function' && this.subtitlesTrack_ && this.subtitlesTrack_.tech_) { 42939 42940 var loadHandler = function loadHandler() { 42941 _this3.handleSegment_(); 42942 }; 42943 42944 this.state = 'WAITING_ON_VTTJS'; 42945 this.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler); 42946 this.subtitlesTrack_.tech_.one('vttjserror', function () { 42947 _this3.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler); 42948 _this3.error({ 42949 message: 'Error loading vtt.js' 42950 }); 42951 _this3.state = 'READY'; 42952 _this3.pause(); 42953 _this3.trigger('error'); 42954 }); 42955 42956 return; 42957 } 42958 42959 segment.requested = true; 42960 42961 try { 42962 this.parseVTTCues_(segmentInfo); 42963 } catch (e) { 42964 this.error({ 42965 message: e.message 42966 }); 42967 this.state = 'READY'; 42968 this.pause(); 42969 return this.trigger('error'); 42970 } 42971 42972 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_); 42973 42974 if (segmentInfo.isSyncRequest) { 42975 this.trigger('syncinfoupdate'); 42976 this.pendingSegment_ = null; 42977 this.state = 'READY'; 42978 return; 42979 } 42980 42981 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 42982 42983 this.mediaSecondsLoaded += segment.duration; 42984 42985 if (segmentInfo.cues.length) { 42986 // remove any overlapping cues to prevent doubling 42987 this.remove(segmentInfo.cues[0].endTime, segmentInfo.cues[segmentInfo.cues.length - 1].endTime); 42988 } 42989 42990 segmentInfo.cues.forEach(function (cue) { 42991 _this3.subtitlesTrack_.addCue(cue); 42992 }); 42993 42994 this.handleUpdateEnd_(); 42995 } 42996 42997 /** 42998 * Uses the WebVTT parser to parse the segment response 42999 * 43000 * @param {Object} segmentInfo 43001 * a segment info object that describes the current segment 43002 * @private 43003 */ 43004 43005 }, { 43006 key: 'parseVTTCues_', 43007 value: function parseVTTCues_(segmentInfo) { 43008 var decoder = void 0; 43009 var decodeBytesToString = false; 43010 43011 if (typeof window_1.TextDecoder === 'function') { 43012 decoder = new window_1.TextDecoder('utf8'); 43013 } else { 43014 decoder = window_1.WebVTT.StringDecoder(); 43015 decodeBytesToString = true; 43016 } 43017 43018 var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, decoder); 43019 43020 segmentInfo.cues = []; 43021 segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 }; 43022 43023 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 43024 parser.ontimestampmap = function (map) { 43025 return segmentInfo.timestampmap = map; 43026 }; 43027 parser.onparsingerror = function (error) { 43028 videojs$1.log.warn('Error encountered when parsing cues: ' + error.message); 43029 }; 43030 43031 if (segmentInfo.segment.map) { 43032 var mapData = segmentInfo.segment.map.bytes; 43033 43034 if (decodeBytesToString) { 43035 mapData = uintToString(mapData); 43036 } 43037 43038 parser.parse(mapData); 43039 } 43040 43041 var segmentData = segmentInfo.bytes; 43042 43043 if (decodeBytesToString) { 43044 segmentData = uintToString(segmentData); 43045 } 43046 43047 parser.parse(segmentData); 43048 parser.flush(); 43049 } 43050 43051 /** 43052 * Updates the start and end times of any cues parsed by the WebVTT parser using 43053 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 43054 * from the SyncController 43055 * 43056 * @param {Object} segmentInfo 43057 * a segment info object that describes the current segment 43058 * @param {Object} mappingObj 43059 * object containing a mapping from TS to media time 43060 * @param {Object} playlist 43061 * the playlist object containing the segment 43062 * @private 43063 */ 43064 43065 }, { 43066 key: 'updateTimeMapping_', 43067 value: function updateTimeMapping_(segmentInfo, mappingObj, playlist) { 43068 var segment = segmentInfo.segment; 43069 43070 if (!mappingObj) { 43071 // If the sync controller does not have a mapping of TS to Media Time for the 43072 // timeline, then we don't have enough information to update the cue 43073 // start/end times 43074 return; 43075 } 43076 43077 if (!segmentInfo.cues.length) { 43078 // If there are no cues, we also do not have enough information to figure out 43079 // segment timing. Mark that the segment contains no cues so we don't re-request 43080 // an empty segment. 43081 segment.empty = true; 43082 return; 43083 } 43084 43085 var timestampmap = segmentInfo.timestampmap; 43086 var diff = timestampmap.MPEGTS / 90000 - timestampmap.LOCAL + mappingObj.mapping; 43087 43088 segmentInfo.cues.forEach(function (cue) { 43089 // First convert cue time to TS time using the timestamp-map provided within the vtt 43090 cue.startTime += diff; 43091 cue.endTime += diff; 43092 }); 43093 43094 if (!playlist.syncInfo) { 43095 var firstStart = segmentInfo.cues[0].startTime; 43096 var lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 43097 43098 playlist.syncInfo = { 43099 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 43100 time: Math.min(firstStart, lastStart - segment.duration) 43101 }; 43102 } 43103 } 43104 }]); 43105 return VTTSegmentLoader; 43106 }(SegmentLoader); 43107 43108 /** 43109 * @file ad-cue-tags.js 43110 */ 43111 43112 /** 43113 * Searches for an ad cue that overlaps with the given mediaTime 43114 */ 43115 var findAdCue = function findAdCue(track, mediaTime) { 43116 var cues = track.cues; 43117 43118 for (var i = 0; i < cues.length; i++) { 43119 var cue = cues[i]; 43120 43121 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 43122 return cue; 43123 } 43124 } 43125 return null; 43126 }; 43127 43128 var updateAdCues = function updateAdCues(media, track) { 43129 var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; 43130 43131 if (!media.segments) { 43132 return; 43133 } 43134 43135 var mediaTime = offset; 43136 var cue = void 0; 43137 43138 for (var i = 0; i < media.segments.length; i++) { 43139 var segment = media.segments[i]; 43140 43141 if (!cue) { 43142 // Since the cues will span for at least the segment duration, adding a fudge 43143 // factor of half segment duration will prevent duplicate cues from being 43144 // created when timing info is not exact (e.g. cue start time initialized 43145 // at 10.006677, but next call mediaTime is 10.003332 ) 43146 cue = findAdCue(track, mediaTime + segment.duration / 2); 43147 } 43148 43149 if (cue) { 43150 if ('cueIn' in segment) { 43151 // Found a CUE-IN so end the cue 43152 cue.endTime = mediaTime; 43153 cue.adEndTime = mediaTime; 43154 mediaTime += segment.duration; 43155 cue = null; 43156 continue; 43157 } 43158 43159 if (mediaTime < cue.endTime) { 43160 // Already processed this mediaTime for this cue 43161 mediaTime += segment.duration; 43162 continue; 43163 } 43164 43165 // otherwise extend cue until a CUE-IN is found 43166 cue.endTime += segment.duration; 43167 } else { 43168 if ('cueOut' in segment) { 43169 cue = new window_1.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); 43170 cue.adStartTime = mediaTime; 43171 // Assumes tag format to be 43172 // #EXT-X-CUE-OUT:30 43173 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 43174 track.addCue(cue); 43175 } 43176 43177 if ('cueOutCont' in segment) { 43178 // Entered into the middle of an ad cue 43179 var adOffset = void 0; 43180 var adTotal = void 0; 43181 43182 // Assumes tag formate to be 43183 // #EXT-X-CUE-OUT-CONT:10/30 43184 43185 var _segment$cueOutCont$s = segment.cueOutCont.split('/').map(parseFloat); 43186 43187 var _segment$cueOutCont$s2 = slicedToArray$1(_segment$cueOutCont$s, 2); 43188 43189 adOffset = _segment$cueOutCont$s2[0]; 43190 adTotal = _segment$cueOutCont$s2[1]; 43191 43192 cue = new window_1.VTTCue(mediaTime, mediaTime + segment.duration, ''); 43193 cue.adStartTime = mediaTime - adOffset; 43194 cue.adEndTime = cue.adStartTime + adTotal; 43195 track.addCue(cue); 43196 } 43197 } 43198 mediaTime += segment.duration; 43199 } 43200 }; 43201 43202 /** 43203 * @file sync-controller.js 43204 */ 43205 43206 var tsprobe = tsInspector.inspect; 43207 43208 var syncPointStrategies = [ 43209 // Stategy "VOD": Handle the VOD-case where the sync-point is *always* 43210 // the equivalence display-time 0 === segment-index 0 43211 { 43212 name: 'VOD', 43213 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43214 if (duration$$1 !== Infinity) { 43215 var syncPoint = { 43216 time: 0, 43217 segmentIndex: 0 43218 }; 43219 43220 return syncPoint; 43221 } 43222 return null; 43223 } 43224 }, 43225 // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 43226 { 43227 name: 'ProgramDateTime', 43228 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43229 if (!syncController.datetimeToDisplayTime) { 43230 return null; 43231 } 43232 43233 var segments = playlist.segments || []; 43234 var syncPoint = null; 43235 var lastDistance = null; 43236 43237 currentTime = currentTime || 0; 43238 43239 for (var i = 0; i < segments.length; i++) { 43240 var segment = segments[i]; 43241 43242 if (segment.dateTimeObject) { 43243 var segmentTime = segment.dateTimeObject.getTime() / 1000; 43244 var segmentStart = segmentTime + syncController.datetimeToDisplayTime; 43245 var distance = Math.abs(currentTime - segmentStart); 43246 43247 // Once the distance begins to increase, we have passed 43248 // currentTime and can stop looking for better candidates 43249 if (lastDistance !== null && lastDistance < distance) { 43250 break; 43251 } 43252 43253 lastDistance = distance; 43254 syncPoint = { 43255 time: segmentStart, 43256 segmentIndex: i 43257 }; 43258 } 43259 } 43260 return syncPoint; 43261 } 43262 }, 43263 // Stategy "Segment": We have a known time mapping for a timeline and a 43264 // segment in the current timeline with timing data 43265 { 43266 name: 'Segment', 43267 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43268 var segments = playlist.segments || []; 43269 var syncPoint = null; 43270 var lastDistance = null; 43271 43272 currentTime = currentTime || 0; 43273 43274 for (var i = 0; i < segments.length; i++) { 43275 var segment = segments[i]; 43276 43277 if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') { 43278 var distance = Math.abs(currentTime - segment.start); 43279 43280 // Once the distance begins to increase, we have passed 43281 // currentTime and can stop looking for better candidates 43282 if (lastDistance !== null && lastDistance < distance) { 43283 break; 43284 } 43285 43286 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 43287 lastDistance = distance; 43288 syncPoint = { 43289 time: segment.start, 43290 segmentIndex: i 43291 }; 43292 } 43293 } 43294 } 43295 return syncPoint; 43296 } 43297 }, 43298 // Stategy "Discontinuity": We have a discontinuity with a known 43299 // display-time 43300 { 43301 name: 'Discontinuity', 43302 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43303 var syncPoint = null; 43304 43305 currentTime = currentTime || 0; 43306 43307 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 43308 var lastDistance = null; 43309 43310 for (var i = 0; i < playlist.discontinuityStarts.length; i++) { 43311 var segmentIndex = playlist.discontinuityStarts[i]; 43312 var discontinuity = playlist.discontinuitySequence + i + 1; 43313 var discontinuitySync = syncController.discontinuities[discontinuity]; 43314 43315 if (discontinuitySync) { 43316 var distance = Math.abs(currentTime - discontinuitySync.time); 43317 43318 // Once the distance begins to increase, we have passed 43319 // currentTime and can stop looking for better candidates 43320 if (lastDistance !== null && lastDistance < distance) { 43321 break; 43322 } 43323 43324 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 43325 lastDistance = distance; 43326 syncPoint = { 43327 time: discontinuitySync.time, 43328 segmentIndex: segmentIndex 43329 }; 43330 } 43331 } 43332 } 43333 } 43334 return syncPoint; 43335 } 43336 }, 43337 // Stategy "Playlist": We have a playlist with a known mapping of 43338 // segment index to display time 43339 { 43340 name: 'Playlist', 43341 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43342 if (playlist.syncInfo) { 43343 var syncPoint = { 43344 time: playlist.syncInfo.time, 43345 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence 43346 }; 43347 43348 return syncPoint; 43349 } 43350 return null; 43351 } 43352 }]; 43353 43354 var SyncController = function (_videojs$EventTarget) { 43355 inherits$3(SyncController, _videojs$EventTarget); 43356 43357 function SyncController() { 43358 classCallCheck$3(this, SyncController); 43359 43360 // Segment Loader state variables... 43361 // ...for synching across variants 43362 var _this = possibleConstructorReturn$3(this, (SyncController.__proto__ || Object.getPrototypeOf(SyncController)).call(this)); 43363 43364 _this.inspectCache_ = undefined; 43365 43366 // ...for synching across variants 43367 _this.timelines = []; 43368 _this.discontinuities = []; 43369 _this.datetimeToDisplayTime = null; 43370 43371 _this.logger_ = logger('SyncController'); 43372 return _this; 43373 } 43374 43375 /** 43376 * Find a sync-point for the playlist specified 43377 * 43378 * A sync-point is defined as a known mapping from display-time to 43379 * a segment-index in the current playlist. 43380 * 43381 * @param {Playlist} playlist 43382 * The playlist that needs a sync-point 43383 * @param {Number} duration 43384 * Duration of the MediaSource (Infinite if playing a live source) 43385 * @param {Number} currentTimeline 43386 * The last timeline from which a segment was loaded 43387 * @returns {Object} 43388 * A sync-point object 43389 */ 43390 43391 createClass$2(SyncController, [{ 43392 key: 'getSyncPoint', 43393 value: function getSyncPoint(playlist, duration$$1, currentTimeline, currentTime) { 43394 var syncPoints = this.runStrategies_(playlist, duration$$1, currentTimeline, currentTime); 43395 43396 if (!syncPoints.length) { 43397 // Signal that we need to attempt to get a sync-point manually 43398 // by fetching a segment in the playlist and constructing 43399 // a sync-point from that information 43400 return null; 43401 } 43402 43403 // Now find the sync-point that is closest to the currentTime because 43404 // that should result in the most accurate guess about which segment 43405 // to fetch 43406 return this.selectSyncPoint_(syncPoints, { key: 'time', value: currentTime }); 43407 } 43408 43409 /** 43410 * Calculate the amount of time that has expired off the playlist during playback 43411 * 43412 * @param {Playlist} playlist 43413 * Playlist object to calculate expired from 43414 * @param {Number} duration 43415 * Duration of the MediaSource (Infinity if playling a live source) 43416 * @returns {Number|null} 43417 * The amount of time that has expired off the playlist during playback. Null 43418 * if no sync-points for the playlist can be found. 43419 */ 43420 43421 }, { 43422 key: 'getExpiredTime', 43423 value: function getExpiredTime(playlist, duration$$1) { 43424 if (!playlist || !playlist.segments) { 43425 return null; 43426 } 43427 43428 var syncPoints = this.runStrategies_(playlist, duration$$1, playlist.discontinuitySequence, 0); 43429 43430 // Without sync-points, there is not enough information to determine the expired time 43431 if (!syncPoints.length) { 43432 return null; 43433 } 43434 43435 var syncPoint = this.selectSyncPoint_(syncPoints, { 43436 key: 'segmentIndex', 43437 value: 0 43438 }); 43439 43440 // If the sync-point is beyond the start of the playlist, we want to subtract the 43441 // duration from index 0 to syncPoint.segmentIndex instead of adding. 43442 if (syncPoint.segmentIndex > 0) { 43443 syncPoint.time *= -1; 43444 } 43445 43446 return Math.abs(syncPoint.time + sumDurations(playlist, syncPoint.segmentIndex, 0)); 43447 } 43448 43449 /** 43450 * Runs each sync-point strategy and returns a list of sync-points returned by the 43451 * strategies 43452 * 43453 * @private 43454 * @param {Playlist} playlist 43455 * The playlist that needs a sync-point 43456 * @param {Number} duration 43457 * Duration of the MediaSource (Infinity if playing a live source) 43458 * @param {Number} currentTimeline 43459 * The last timeline from which a segment was loaded 43460 * @returns {Array} 43461 * A list of sync-point objects 43462 */ 43463 43464 }, { 43465 key: 'runStrategies_', 43466 value: function runStrategies_(playlist, duration$$1, currentTimeline, currentTime) { 43467 var syncPoints = []; 43468 43469 // Try to find a sync-point in by utilizing various strategies... 43470 for (var i = 0; i < syncPointStrategies.length; i++) { 43471 var strategy = syncPointStrategies[i]; 43472 var syncPoint = strategy.run(this, playlist, duration$$1, currentTimeline, currentTime); 43473 43474 if (syncPoint) { 43475 syncPoint.strategy = strategy.name; 43476 syncPoints.push({ 43477 strategy: strategy.name, 43478 syncPoint: syncPoint 43479 }); 43480 } 43481 } 43482 43483 return syncPoints; 43484 } 43485 43486 /** 43487 * Selects the sync-point nearest the specified target 43488 * 43489 * @private 43490 * @param {Array} syncPoints 43491 * List of sync-points to select from 43492 * @param {Object} target 43493 * Object specifying the property and value we are targeting 43494 * @param {String} target.key 43495 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 43496 * @param {Number} target.value 43497 * The value to target for the specified key. 43498 * @returns {Object} 43499 * The sync-point nearest the target 43500 */ 43501 43502 }, { 43503 key: 'selectSyncPoint_', 43504 value: function selectSyncPoint_(syncPoints, target) { 43505 var bestSyncPoint = syncPoints[0].syncPoint; 43506 var bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 43507 var bestStrategy = syncPoints[0].strategy; 43508 43509 for (var i = 1; i < syncPoints.length; i++) { 43510 var newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 43511 43512 if (newDistance < bestDistance) { 43513 bestDistance = newDistance; 43514 bestSyncPoint = syncPoints[i].syncPoint; 43515 bestStrategy = syncPoints[i].strategy; 43516 } 43517 } 43518 43519 this.logger_('syncPoint for [' + target.key + ': ' + target.value + '] chosen with strategy' + (' [' + bestStrategy + ']: [time:' + bestSyncPoint.time + ',') + (' segmentIndex:' + bestSyncPoint.segmentIndex + ']')); 43520 43521 return bestSyncPoint; 43522 } 43523 43524 /** 43525 * Save any meta-data present on the segments when segments leave 43526 * the live window to the playlist to allow for synchronization at the 43527 * playlist level later. 43528 * 43529 * @param {Playlist} oldPlaylist - The previous active playlist 43530 * @param {Playlist} newPlaylist - The updated and most current playlist 43531 */ 43532 43533 }, { 43534 key: 'saveExpiredSegmentInfo', 43535 value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 43536 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 43537 43538 // When a segment expires from the playlist and it has a start time 43539 // save that information as a possible sync-point reference in future 43540 for (var i = mediaSequenceDiff - 1; i >= 0; i--) { 43541 var lastRemovedSegment = oldPlaylist.segments[i]; 43542 43543 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 43544 newPlaylist.syncInfo = { 43545 mediaSequence: oldPlaylist.mediaSequence + i, 43546 time: lastRemovedSegment.start 43547 }; 43548 this.logger_('playlist refresh sync: [time:' + newPlaylist.syncInfo.time + ',' + (' mediaSequence: ' + newPlaylist.syncInfo.mediaSequence + ']')); 43549 this.trigger('syncinfoupdate'); 43550 break; 43551 } 43552 } 43553 } 43554 43555 /** 43556 * Save the mapping from playlist's ProgramDateTime to display. This should 43557 * only ever happen once at the start of playback. 43558 * 43559 * @param {Playlist} playlist - The currently active playlist 43560 */ 43561 43562 }, { 43563 key: 'setDateTimeMapping', 43564 value: function setDateTimeMapping(playlist) { 43565 if (!this.datetimeToDisplayTime && playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 43566 var playlistTimestamp = playlist.segments[0].dateTimeObject.getTime() / 1000; 43567 43568 this.datetimeToDisplayTime = -playlistTimestamp; 43569 } 43570 } 43571 43572 /** 43573 * Reset the state of the inspection cache when we do a rendition 43574 * switch 43575 */ 43576 43577 }, { 43578 key: 'reset', 43579 value: function reset() { 43580 this.inspectCache_ = undefined; 43581 } 43582 43583 /** 43584 * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start 43585 * and end of the segment in it's internal "media time". Used to generate 43586 * mappings from that internal "media time" to the display time that is 43587 * shown on the player. 43588 * 43589 * @param {SegmentInfo} segmentInfo - The current active request information 43590 */ 43591 43592 }, { 43593 key: 'probeSegmentInfo', 43594 value: function probeSegmentInfo(segmentInfo) { 43595 var segment = segmentInfo.segment; 43596 var playlist = segmentInfo.playlist; 43597 var timingInfo = void 0; 43598 43599 if (segment.map) { 43600 timingInfo = this.probeMp4Segment_(segmentInfo); 43601 } else { 43602 timingInfo = this.probeTsSegment_(segmentInfo); 43603 } 43604 43605 if (timingInfo) { 43606 if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) { 43607 this.saveDiscontinuitySyncInfo_(segmentInfo); 43608 43609 // If the playlist does not have sync information yet, record that information 43610 // now with segment timing information 43611 if (!playlist.syncInfo) { 43612 playlist.syncInfo = { 43613 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 43614 time: segment.start 43615 }; 43616 } 43617 } 43618 } 43619 43620 return timingInfo; 43621 } 43622 43623 /** 43624 * Probe an fmp4 or an mpeg2-ts segment to determine the start of the segment 43625 * in it's internal "media time". 43626 * 43627 * @private 43628 * @param {SegmentInfo} segmentInfo - The current active request information 43629 * @return {object} The start and end time of the current segment in "media time" 43630 */ 43631 43632 }, { 43633 key: 'probeMp4Segment_', 43634 value: function probeMp4Segment_(segmentInfo) { 43635 var segment = segmentInfo.segment; 43636 var timescales = probe.timescale(segment.map.bytes); 43637 var startTime = probe.startTime(timescales, segmentInfo.bytes); 43638 43639 if (segmentInfo.timestampOffset !== null) { 43640 segmentInfo.timestampOffset -= startTime; 43641 } 43642 43643 return { 43644 start: startTime, 43645 end: startTime + segment.duration 43646 }; 43647 } 43648 43649 /** 43650 * Probe an mpeg2-ts segment to determine the start and end of the segment 43651 * in it's internal "media time". 43652 * 43653 * @private 43654 * @param {SegmentInfo} segmentInfo - The current active request information 43655 * @return {object} The start and end time of the current segment in "media time" 43656 */ 43657 43658 }, { 43659 key: 'probeTsSegment_', 43660 value: function probeTsSegment_(segmentInfo) { 43661 var timeInfo = tsprobe(segmentInfo.bytes, this.inspectCache_); 43662 var segmentStartTime = void 0; 43663 var segmentEndTime = void 0; 43664 43665 if (!timeInfo) { 43666 return null; 43667 } 43668 43669 if (timeInfo.video && timeInfo.video.length === 2) { 43670 this.inspectCache_ = timeInfo.video[1].dts; 43671 segmentStartTime = timeInfo.video[0].dtsTime; 43672 segmentEndTime = timeInfo.video[1].dtsTime; 43673 } else if (timeInfo.audio && timeInfo.audio.length === 2) { 43674 this.inspectCache_ = timeInfo.audio[1].dts; 43675 segmentStartTime = timeInfo.audio[0].dtsTime; 43676 segmentEndTime = timeInfo.audio[1].dtsTime; 43677 } 43678 43679 return { 43680 start: segmentStartTime, 43681 end: segmentEndTime, 43682 containsVideo: timeInfo.video && timeInfo.video.length === 2, 43683 containsAudio: timeInfo.audio && timeInfo.audio.length === 2 43684 }; 43685 } 43686 }, { 43687 key: 'timestampOffsetForTimeline', 43688 value: function timestampOffsetForTimeline(timeline) { 43689 if (typeof this.timelines[timeline] === 'undefined') { 43690 return null; 43691 } 43692 return this.timelines[timeline].time; 43693 } 43694 }, { 43695 key: 'mappingForTimeline', 43696 value: function mappingForTimeline(timeline) { 43697 if (typeof this.timelines[timeline] === 'undefined') { 43698 return null; 43699 } 43700 return this.timelines[timeline].mapping; 43701 } 43702 43703 /** 43704 * Use the "media time" for a segment to generate a mapping to "display time" and 43705 * save that display time to the segment. 43706 * 43707 * @private 43708 * @param {SegmentInfo} segmentInfo 43709 * The current active request information 43710 * @param {object} timingInfo 43711 * The start and end time of the current segment in "media time" 43712 * @returns {Boolean} 43713 * Returns false if segment time mapping could not be calculated 43714 */ 43715 43716 }, { 43717 key: 'calculateSegmentTimeMapping_', 43718 value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) { 43719 var segment = segmentInfo.segment; 43720 var mappingObj = this.timelines[segmentInfo.timeline]; 43721 43722 if (segmentInfo.timestampOffset !== null) { 43723 mappingObj = { 43724 time: segmentInfo.startOfSegment, 43725 mapping: segmentInfo.startOfSegment - timingInfo.start 43726 }; 43727 this.timelines[segmentInfo.timeline] = mappingObj; 43728 this.trigger('timestampoffset'); 43729 43730 this.logger_('time mapping for timeline ' + segmentInfo.timeline + ': ' + ('[time: ' + mappingObj.time + '] [mapping: ' + mappingObj.mapping + ']')); 43731 43732 segment.start = segmentInfo.startOfSegment; 43733 segment.end = timingInfo.end + mappingObj.mapping; 43734 } else if (mappingObj) { 43735 segment.start = timingInfo.start + mappingObj.mapping; 43736 segment.end = timingInfo.end + mappingObj.mapping; 43737 } else { 43738 return false; 43739 } 43740 43741 return true; 43742 } 43743 43744 /** 43745 * Each time we have discontinuity in the playlist, attempt to calculate the location 43746 * in display of the start of the discontinuity and save that. We also save an accuracy 43747 * value so that we save values with the most accuracy (closest to 0.) 43748 * 43749 * @private 43750 * @param {SegmentInfo} segmentInfo - The current active request information 43751 */ 43752 43753 }, { 43754 key: 'saveDiscontinuitySyncInfo_', 43755 value: function saveDiscontinuitySyncInfo_(segmentInfo) { 43756 var playlist = segmentInfo.playlist; 43757 var segment = segmentInfo.segment; 43758 43759 // If the current segment is a discontinuity then we know exactly where 43760 // the start of the range and it's accuracy is 0 (greater accuracy values 43761 // mean more approximation) 43762 if (segment.discontinuity) { 43763 this.discontinuities[segment.timeline] = { 43764 time: segment.start, 43765 accuracy: 0 43766 }; 43767 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 43768 // Search for future discontinuities that we can provide better timing 43769 // information for and save that information for sync purposes 43770 for (var i = 0; i < playlist.discontinuityStarts.length; i++) { 43771 var segmentIndex = playlist.discontinuityStarts[i]; 43772 var discontinuity = playlist.discontinuitySequence + i + 1; 43773 var mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 43774 var accuracy = Math.abs(mediaIndexDiff); 43775 43776 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) { 43777 var time = void 0; 43778 43779 if (mediaIndexDiff < 0) { 43780 time = segment.start - sumDurations(playlist, segmentInfo.mediaIndex, segmentIndex); 43781 } else { 43782 time = segment.end + sumDurations(playlist, segmentInfo.mediaIndex + 1, segmentIndex); 43783 } 43784 43785 this.discontinuities[discontinuity] = { 43786 time: time, 43787 accuracy: accuracy 43788 }; 43789 } 43790 } 43791 } 43792 } 43793 }]); 43794 return SyncController; 43795 }(videojs$1.EventTarget); 43796 43797 var Decrypter$1 = new shimWorker("./decrypter-worker.worker.js", function (window, document$$1) { 43798 var self = this; 43799 var decrypterWorker = function () { 43800 43801 var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 43802 43803 var win; 43804 43805 if (typeof window !== "undefined") { 43806 win = window; 43807 } else if (typeof commonjsGlobal !== "undefined") { 43808 win = commonjsGlobal; 43809 } else if (typeof self !== "undefined") { 43810 win = self; 43811 } else { 43812 win = {}; 43813 } 43814 43815 var window_1$$1 = win; 43816 43817 /* 43818 * pkcs7.pad 43819 * https://github.com/brightcove/pkcs7 43820 * 43821 * Copyright (c) 2014 Brightcove 43822 * Licensed under the apache2 license. 43823 */ 43824 43825 /** 43826 * Returns the subarray of a Uint8Array without PKCS#7 padding. 43827 * @param padded {Uint8Array} unencrypted bytes that have been padded 43828 * @return {Uint8Array} the unpadded bytes 43829 * @see http://tools.ietf.org/html/rfc5652 43830 */ 43831 function unpad(padded) { 43832 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 43833 } 43834 43835 var classCallCheck$$1 = function classCallCheck$$1(instance, Constructor) { 43836 if (!(instance instanceof Constructor)) { 43837 throw new TypeError("Cannot call a class as a function"); 43838 } 43839 }; 43840 43841 var createClass$$1 = function () { 43842 function defineProperties(target, props) { 43843 for (var i = 0; i < props.length; i++) { 43844 var descriptor = props[i]; 43845 descriptor.enumerable = descriptor.enumerable || false; 43846 descriptor.configurable = true; 43847 if ("value" in descriptor) descriptor.writable = true; 43848 Object.defineProperty(target, descriptor.key, descriptor); 43849 } 43850 } 43851 43852 return function (Constructor, protoProps, staticProps) { 43853 if (protoProps) defineProperties(Constructor.prototype, protoProps); 43854 if (staticProps) defineProperties(Constructor, staticProps); 43855 return Constructor; 43856 }; 43857 }(); 43858 43859 var inherits$$1 = function inherits$$1(subClass, superClass) { 43860 if (typeof superClass !== "function" && superClass !== null) { 43861 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === 'undefined' ? 'undefined' : _typeof(superClass))); 43862 } 43863 43864 subClass.prototype = Object.create(superClass && superClass.prototype, { 43865 constructor: { 43866 value: subClass, 43867 enumerable: false, 43868 writable: true, 43869 configurable: true 43870 } 43871 }); 43872 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 43873 }; 43874 43875 var possibleConstructorReturn$$1 = function possibleConstructorReturn$$1(self, call) { 43876 if (!self) { 43877 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 43878 } 43879 43880 return call && ((typeof call === 'undefined' ? 'undefined' : _typeof(call)) === "object" || typeof call === "function") ? call : self; 43881 }; 43882 43883 /** 43884 * @file aes.js 43885 * 43886 * This file contains an adaptation of the AES decryption algorithm 43887 * from the Standford Javascript Cryptography Library. That work is 43888 * covered by the following copyright and permissions notice: 43889 * 43890 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 43891 * All rights reserved. 43892 * 43893 * Redistribution and use in source and binary forms, with or without 43894 * modification, are permitted provided that the following conditions are 43895 * met: 43896 * 43897 * 1. Redistributions of source code must retain the above copyright 43898 * notice, this list of conditions and the following disclaimer. 43899 * 43900 * 2. Redistributions in binary form must reproduce the above 43901 * copyright notice, this list of conditions and the following 43902 * disclaimer in the documentation and/or other materials provided 43903 * with the distribution. 43904 * 43905 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 43906 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 43907 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 43908 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 43909 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 43910 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 43911 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 43912 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 43913 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 43914 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 43915 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43916 * 43917 * The views and conclusions contained in the software and documentation 43918 * are those of the authors and should not be interpreted as representing 43919 * official policies, either expressed or implied, of the authors. 43920 */ 43921 43922 /** 43923 * Expand the S-box tables. 43924 * 43925 * @private 43926 */ 43927 var precompute = function precompute() { 43928 var tables = [[[], [], [], [], []], [[], [], [], [], []]]; 43929 var encTable = tables[0]; 43930 var decTable = tables[1]; 43931 var sbox = encTable[4]; 43932 var sboxInv = decTable[4]; 43933 var i = void 0; 43934 var x = void 0; 43935 var xInv = void 0; 43936 var d = []; 43937 var th = []; 43938 var x2 = void 0; 43939 var x4 = void 0; 43940 var x8 = void 0; 43941 var s = void 0; 43942 var tEnc = void 0; 43943 var tDec = void 0; 43944 43945 // Compute double and third tables 43946 for (i = 0; i < 256; i++) { 43947 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 43948 } 43949 43950 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 43951 // Compute sbox 43952 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 43953 s = s >> 8 ^ s & 255 ^ 99; 43954 sbox[x] = s; 43955 sboxInv[s] = x; 43956 43957 // Compute MixColumns 43958 x8 = d[x4 = d[x2 = d[x]]]; 43959 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 43960 tEnc = d[s] * 0x101 ^ s * 0x1010100; 43961 43962 for (i = 0; i < 4; i++) { 43963 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 43964 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 43965 } 43966 } 43967 43968 // Compactify. Considerable speedup on Firefox. 43969 for (i = 0; i < 5; i++) { 43970 encTable[i] = encTable[i].slice(0); 43971 decTable[i] = decTable[i].slice(0); 43972 } 43973 return tables; 43974 }; 43975 var aesTables = null; 43976 43977 /** 43978 * Schedule out an AES key for both encryption and decryption. This 43979 * is a low-level class. Use a cipher mode to do bulk encryption. 43980 * 43981 * @class AES 43982 * @param key {Array} The key as an array of 4, 6 or 8 words. 43983 */ 43984 43985 var AES = function () { 43986 function AES(key) { 43987 classCallCheck$$1(this, AES); 43988 43989 /** 43990 * The expanded S-box and inverse S-box tables. These will be computed 43991 * on the client so that we don't have to send them down the wire. 43992 * 43993 * There are two tables, _tables[0] is for encryption and 43994 * _tables[1] is for decryption. 43995 * 43996 * The first 4 sub-tables are the expanded S-box with MixColumns. The 43997 * last (_tables[01][4]) is the S-box itself. 43998 * 43999 * @private 44000 */ 44001 // if we have yet to precompute the S-box tables 44002 // do so now 44003 if (!aesTables) { 44004 aesTables = precompute(); 44005 } 44006 // then make a copy of that object for use 44007 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()]]; 44008 var i = void 0; 44009 var j = void 0; 44010 var tmp = void 0; 44011 var encKey = void 0; 44012 var decKey = void 0; 44013 var sbox = this._tables[0][4]; 44014 var decTable = this._tables[1]; 44015 var keyLen = key.length; 44016 var rcon = 1; 44017 44018 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 44019 throw new Error('Invalid aes key size'); 44020 } 44021 44022 encKey = key.slice(0); 44023 decKey = []; 44024 this._key = [encKey, decKey]; 44025 44026 // schedule encryption keys 44027 for (i = keyLen; i < 4 * keyLen + 28; i++) { 44028 tmp = encKey[i - 1]; 44029 44030 // apply sbox 44031 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 44032 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; 44033 44034 // shift rows and add rcon 44035 if (i % keyLen === 0) { 44036 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 44037 rcon = rcon << 1 ^ (rcon >> 7) * 283; 44038 } 44039 } 44040 44041 encKey[i] = encKey[i - keyLen] ^ tmp; 44042 } 44043 44044 // schedule decryption keys 44045 for (j = 0; i; j++, i--) { 44046 tmp = encKey[j & 3 ? i : i - 4]; 44047 if (i <= 4 || j < 4) { 44048 decKey[j] = tmp; 44049 } else { 44050 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 44051 } 44052 } 44053 } 44054 44055 /** 44056 * Decrypt 16 bytes, specified as four 32-bit words. 44057 * 44058 * @param {Number} encrypted0 the first word to decrypt 44059 * @param {Number} encrypted1 the second word to decrypt 44060 * @param {Number} encrypted2 the third word to decrypt 44061 * @param {Number} encrypted3 the fourth word to decrypt 44062 * @param {Int32Array} out the array to write the decrypted words 44063 * into 44064 * @param {Number} offset the offset into the output array to start 44065 * writing results 44066 * @return {Array} The plaintext. 44067 */ 44068 44069 AES.prototype.decrypt = function decrypt$$1(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 44070 var key = this._key[1]; 44071 // state variables a,b,c,d are loaded with pre-whitened data 44072 var a = encrypted0 ^ key[0]; 44073 var b = encrypted3 ^ key[1]; 44074 var c = encrypted2 ^ key[2]; 44075 var d = encrypted1 ^ key[3]; 44076 var a2 = void 0; 44077 var b2 = void 0; 44078 var c2 = void 0; 44079 44080 // key.length === 2 ? 44081 var nInnerRounds = key.length / 4 - 2; 44082 var i = void 0; 44083 var kIndex = 4; 44084 var table = this._tables[1]; 44085 44086 // load up the tables 44087 var table0 = table[0]; 44088 var table1 = table[1]; 44089 var table2 = table[2]; 44090 var table3 = table[3]; 44091 var sbox = table[4]; 44092 44093 // Inner rounds. Cribbed from OpenSSL. 44094 for (i = 0; i < nInnerRounds; i++) { 44095 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 44096 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 44097 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 44098 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 44099 kIndex += 4; 44100 a = a2;b = b2;c = c2; 44101 } 44102 44103 // Last round. 44104 for (i = 0; i < 4; i++) { 44105 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 44106 a2 = a;a = b;b = c;c = d;d = a2; 44107 } 44108 }; 44109 44110 return AES; 44111 }(); 44112 44113 /** 44114 * @file stream.js 44115 */ 44116 /** 44117 * A lightweight readable stream implemention that handles event dispatching. 44118 * 44119 * @class Stream 44120 */ 44121 var Stream = function () { 44122 function Stream() { 44123 classCallCheck$$1(this, Stream); 44124 44125 this.listeners = {}; 44126 } 44127 44128 /** 44129 * Add a listener for a specified event type. 44130 * 44131 * @param {String} type the event name 44132 * @param {Function} listener the callback to be invoked when an event of 44133 * the specified type occurs 44134 */ 44135 44136 Stream.prototype.on = function on(type, listener) { 44137 if (!this.listeners[type]) { 44138 this.listeners[type] = []; 44139 } 44140 this.listeners[type].push(listener); 44141 }; 44142 44143 /** 44144 * Remove a listener for a specified event type. 44145 * 44146 * @param {String} type the event name 44147 * @param {Function} listener a function previously registered for this 44148 * type of event through `on` 44149 * @return {Boolean} if we could turn it off or not 44150 */ 44151 44152 Stream.prototype.off = function off(type, listener) { 44153 if (!this.listeners[type]) { 44154 return false; 44155 } 44156 44157 var index = this.listeners[type].indexOf(listener); 44158 44159 this.listeners[type].splice(index, 1); 44160 return index > -1; 44161 }; 44162 44163 /** 44164 * Trigger an event of the specified type on this stream. Any additional 44165 * arguments to this function are passed as parameters to event listeners. 44166 * 44167 * @param {String} type the event name 44168 */ 44169 44170 Stream.prototype.trigger = function trigger(type) { 44171 var callbacks = this.listeners[type]; 44172 44173 if (!callbacks) { 44174 return; 44175 } 44176 44177 // Slicing the arguments on every invocation of this method 44178 // can add a significant amount of overhead. Avoid the 44179 // intermediate object creation for the common case of a 44180 // single callback argument 44181 if (arguments.length === 2) { 44182 var length = callbacks.length; 44183 44184 for (var i = 0; i < length; ++i) { 44185 callbacks[i].call(this, arguments[1]); 44186 } 44187 } else { 44188 var args = Array.prototype.slice.call(arguments, 1); 44189 var _length = callbacks.length; 44190 44191 for (var _i = 0; _i < _length; ++_i) { 44192 callbacks[_i].apply(this, args); 44193 } 44194 } 44195 }; 44196 44197 /** 44198 * Destroys the stream and cleans up. 44199 */ 44200 44201 Stream.prototype.dispose = function dispose() { 44202 this.listeners = {}; 44203 }; 44204 /** 44205 * Forwards all `data` events on this stream to the destination stream. The 44206 * destination stream should provide a method `push` to receive the data 44207 * events as they arrive. 44208 * 44209 * @param {Stream} destination the stream that will receive all `data` events 44210 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 44211 */ 44212 44213 Stream.prototype.pipe = function pipe(destination) { 44214 this.on('data', function (data) { 44215 destination.push(data); 44216 }); 44217 }; 44218 44219 return Stream; 44220 }(); 44221 44222 /** 44223 * @file async-stream.js 44224 */ 44225 /** 44226 * A wrapper around the Stream class to use setTiemout 44227 * and run stream "jobs" Asynchronously 44228 * 44229 * @class AsyncStream 44230 * @extends Stream 44231 */ 44232 44233 var AsyncStream$$1 = function (_Stream) { 44234 inherits$$1(AsyncStream$$1, _Stream); 44235 44236 function AsyncStream$$1() { 44237 classCallCheck$$1(this, AsyncStream$$1); 44238 44239 var _this = possibleConstructorReturn$$1(this, _Stream.call(this, Stream)); 44240 44241 _this.jobs = []; 44242 _this.delay = 1; 44243 _this.timeout_ = null; 44244 return _this; 44245 } 44246 44247 /** 44248 * process an async job 44249 * 44250 * @private 44251 */ 44252 44253 AsyncStream$$1.prototype.processJob_ = function processJob_() { 44254 this.jobs.shift()(); 44255 if (this.jobs.length) { 44256 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 44257 } else { 44258 this.timeout_ = null; 44259 } 44260 }; 44261 44262 /** 44263 * push a job into the stream 44264 * 44265 * @param {Function} job the job to push into the stream 44266 */ 44267 44268 AsyncStream$$1.prototype.push = function push(job) { 44269 this.jobs.push(job); 44270 if (!this.timeout_) { 44271 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 44272 } 44273 }; 44274 44275 return AsyncStream$$1; 44276 }(Stream); 44277 44278 /** 44279 * @file decrypter.js 44280 * 44281 * An asynchronous implementation of AES-128 CBC decryption with 44282 * PKCS#7 padding. 44283 */ 44284 44285 /** 44286 * Convert network-order (big-endian) bytes into their little-endian 44287 * representation. 44288 */ 44289 var ntoh = function ntoh(word) { 44290 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 44291 }; 44292 44293 /** 44294 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 44295 * 44296 * @param {Uint8Array} encrypted the encrypted bytes 44297 * @param {Uint32Array} key the bytes of the decryption key 44298 * @param {Uint32Array} initVector the initialization vector (IV) to 44299 * use for the first round of CBC. 44300 * @return {Uint8Array} the decrypted bytes 44301 * 44302 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 44303 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 44304 * @see https://tools.ietf.org/html/rfc2315 44305 */ 44306 var decrypt$$1 = function decrypt$$1(encrypted, key, initVector) { 44307 // word-level access to the encrypted bytes 44308 var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 44309 44310 var decipher = new AES(Array.prototype.slice.call(key)); 44311 44312 // byte and word-level access for the decrypted output 44313 var decrypted = new Uint8Array(encrypted.byteLength); 44314 var decrypted32 = new Int32Array(decrypted.buffer); 44315 44316 // temporary variables for working with the IV, encrypted, and 44317 // decrypted data 44318 var init0 = void 0; 44319 var init1 = void 0; 44320 var init2 = void 0; 44321 var init3 = void 0; 44322 var encrypted0 = void 0; 44323 var encrypted1 = void 0; 44324 var encrypted2 = void 0; 44325 var encrypted3 = void 0; 44326 44327 // iteration variable 44328 var wordIx = void 0; 44329 44330 // pull out the words of the IV to ensure we don't modify the 44331 // passed-in reference and easier access 44332 init0 = initVector[0]; 44333 init1 = initVector[1]; 44334 init2 = initVector[2]; 44335 init3 = initVector[3]; 44336 44337 // decrypt four word sequences, applying cipher-block chaining (CBC) 44338 // to each decrypted block 44339 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 44340 // convert big-endian (network order) words into little-endian 44341 // (javascript order) 44342 encrypted0 = ntoh(encrypted32[wordIx]); 44343 encrypted1 = ntoh(encrypted32[wordIx + 1]); 44344 encrypted2 = ntoh(encrypted32[wordIx + 2]); 44345 encrypted3 = ntoh(encrypted32[wordIx + 3]); 44346 44347 // decrypt the block 44348 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); 44349 44350 // XOR with the IV, and restore network byte-order to obtain the 44351 // plaintext 44352 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 44353 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 44354 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 44355 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); 44356 44357 // setup the IV for the next round 44358 init0 = encrypted0; 44359 init1 = encrypted1; 44360 init2 = encrypted2; 44361 init3 = encrypted3; 44362 } 44363 44364 return decrypted; 44365 }; 44366 44367 /** 44368 * The `Decrypter` class that manages decryption of AES 44369 * data through `AsyncStream` objects and the `decrypt` 44370 * function 44371 * 44372 * @param {Uint8Array} encrypted the encrypted bytes 44373 * @param {Uint32Array} key the bytes of the decryption key 44374 * @param {Uint32Array} initVector the initialization vector (IV) to 44375 * @param {Function} done the function to run when done 44376 * @class Decrypter 44377 */ 44378 44379 var Decrypter$$1 = function () { 44380 function Decrypter$$1(encrypted, key, initVector, done) { 44381 classCallCheck$$1(this, Decrypter$$1); 44382 44383 var step = Decrypter$$1.STEP; 44384 var encrypted32 = new Int32Array(encrypted.buffer); 44385 var decrypted = new Uint8Array(encrypted.byteLength); 44386 var i = 0; 44387 44388 this.asyncStream_ = new AsyncStream$$1(); 44389 44390 // split up the encryption job and do the individual chunks asynchronously 44391 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 44392 for (i = step; i < encrypted32.length; i += step) { 44393 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 44394 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 44395 } 44396 // invoke the done() callback when everything is finished 44397 this.asyncStream_.push(function () { 44398 // remove pkcs#7 padding from the decrypted bytes 44399 done(null, unpad(decrypted)); 44400 }); 44401 } 44402 44403 /** 44404 * a getter for step the maximum number of bytes to process at one time 44405 * 44406 * @return {Number} the value of step 32000 44407 */ 44408 44409 /** 44410 * @private 44411 */ 44412 Decrypter$$1.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { 44413 return function () { 44414 var bytes = decrypt$$1(encrypted, key, initVector); 44415 44416 decrypted.set(bytes, encrypted.byteOffset); 44417 }; 44418 }; 44419 44420 createClass$$1(Decrypter$$1, null, [{ 44421 key: 'STEP', 44422 get: function get$$1() { 44423 // 4 * 8000; 44424 return 32000; 44425 } 44426 }]); 44427 return Decrypter$$1; 44428 }(); 44429 44430 /** 44431 * @file bin-utils.js 44432 */ 44433 44434 /** 44435 * Creates an object for sending to a web worker modifying properties that are TypedArrays 44436 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 44437 * 44438 * @param {Object} message 44439 * Object of properties and values to send to the web worker 44440 * @return {Object} 44441 * Modified message with TypedArray values expanded 44442 * @function createTransferableMessage 44443 */ 44444 var createTransferableMessage = function createTransferableMessage(message) { 44445 var transferable = {}; 44446 44447 Object.keys(message).forEach(function (key) { 44448 var value = message[key]; 44449 44450 if (ArrayBuffer.isView(value)) { 44451 transferable[key] = { 44452 bytes: value.buffer, 44453 byteOffset: value.byteOffset, 44454 byteLength: value.byteLength 44455 }; 44456 } else { 44457 transferable[key] = value; 44458 } 44459 }); 44460 44461 return transferable; 44462 }; 44463 44464 /** 44465 * Our web worker interface so that things can talk to aes-decrypter 44466 * that will be running in a web worker. the scope is passed to this by 44467 * webworkify. 44468 * 44469 * @param {Object} self 44470 * the scope for the web worker 44471 */ 44472 var DecrypterWorker = function DecrypterWorker(self) { 44473 self.onmessage = function (event) { 44474 var data = event.data; 44475 var encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength); 44476 var key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 44477 var iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 44478 44479 /* eslint-disable no-new, handle-callback-err */ 44480 new Decrypter$$1(encrypted, key, iv, function (err, bytes) { 44481 window_1$$1.postMessage(createTransferableMessage({ 44482 source: data.source, 44483 decrypted: bytes 44484 }), [bytes.buffer]); 44485 }); 44486 /* eslint-enable */ 44487 }; 44488 }; 44489 44490 var decrypterWorker = new DecrypterWorker(self); 44491 44492 return decrypterWorker; 44493 }(); 44494 }); 44495 44496 /** 44497 * Convert the properties of an HLS track into an audioTrackKind. 44498 * 44499 * @private 44500 */ 44501 var audioTrackKind_ = function audioTrackKind_(properties) { 44502 var kind = properties.default ? 'main' : 'alternative'; 44503 44504 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) { 44505 kind = 'main-desc'; 44506 } 44507 44508 return kind; 44509 }; 44510 44511 /** 44512 * Pause provided segment loader and playlist loader if active 44513 * 44514 * @param {SegmentLoader} segmentLoader 44515 * SegmentLoader to pause 44516 * @param {Object} mediaType 44517 * Active media type 44518 * @function stopLoaders 44519 */ 44520 var stopLoaders = function stopLoaders(segmentLoader, mediaType) { 44521 segmentLoader.abort(); 44522 segmentLoader.pause(); 44523 44524 if (mediaType && mediaType.activePlaylistLoader) { 44525 mediaType.activePlaylistLoader.pause(); 44526 mediaType.activePlaylistLoader = null; 44527 } 44528 }; 44529 44530 /** 44531 * Start loading provided segment loader and playlist loader 44532 * 44533 * @param {PlaylistLoader} playlistLoader 44534 * PlaylistLoader to start loading 44535 * @param {Object} mediaType 44536 * Active media type 44537 * @function startLoaders 44538 */ 44539 var startLoaders = function startLoaders(playlistLoader, mediaType) { 44540 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 44541 // playlist loader 44542 mediaType.activePlaylistLoader = playlistLoader; 44543 playlistLoader.load(); 44544 }; 44545 44546 /** 44547 * Returns a function to be called when the media group changes. It performs a 44548 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 44549 * change of group is merely a rendition switch of the same content at another encoding, 44550 * rather than a change of content, such as switching audio from English to Spanish. 44551 * 44552 * @param {String} type 44553 * MediaGroup type 44554 * @param {Object} settings 44555 * Object containing required information for media groups 44556 * @return {Function} 44557 * Handler for a non-destructive resync of SegmentLoader when the active media 44558 * group changes. 44559 * @function onGroupChanged 44560 */ 44561 var onGroupChanged = function onGroupChanged(type, settings) { 44562 return function () { 44563 var _settings$segmentLoad = settings.segmentLoaders, 44564 segmentLoader = _settings$segmentLoad[type], 44565 mainSegmentLoader = _settings$segmentLoad.main, 44566 mediaType = settings.mediaTypes[type]; 44567 44568 var activeTrack = mediaType.activeTrack(); 44569 var activeGroup = mediaType.activeGroup(activeTrack); 44570 var previousActiveLoader = mediaType.activePlaylistLoader; 44571 44572 stopLoaders(segmentLoader, mediaType); 44573 44574 if (!activeGroup) { 44575 // there is no group active 44576 return; 44577 } 44578 44579 if (!activeGroup.playlistLoader) { 44580 if (previousActiveLoader) { 44581 // The previous group had a playlist loader but the new active group does not 44582 // this means we are switching from demuxed to muxed audio. In this case we want to 44583 // do a destructive reset of the main segment loader and not restart the audio 44584 // loaders. 44585 mainSegmentLoader.resetEverything(); 44586 } 44587 return; 44588 } 44589 44590 // Non-destructive resync 44591 segmentLoader.resyncLoader(); 44592 44593 startLoaders(activeGroup.playlistLoader, mediaType); 44594 }; 44595 }; 44596 44597 /** 44598 * Returns a function to be called when the media track changes. It performs a 44599 * destructive reset of the SegmentLoader to ensure we start loading as close to 44600 * currentTime as possible. 44601 * 44602 * @param {String} type 44603 * MediaGroup type 44604 * @param {Object} settings 44605 * Object containing required information for media groups 44606 * @return {Function} 44607 * Handler for a destructive reset of SegmentLoader when the active media 44608 * track changes. 44609 * @function onTrackChanged 44610 */ 44611 var onTrackChanged = function onTrackChanged(type, settings) { 44612 return function () { 44613 var _settings$segmentLoad2 = settings.segmentLoaders, 44614 segmentLoader = _settings$segmentLoad2[type], 44615 mainSegmentLoader = _settings$segmentLoad2.main, 44616 mediaType = settings.mediaTypes[type]; 44617 44618 var activeTrack = mediaType.activeTrack(); 44619 var activeGroup = mediaType.activeGroup(activeTrack); 44620 var previousActiveLoader = mediaType.activePlaylistLoader; 44621 44622 stopLoaders(segmentLoader, mediaType); 44623 44624 if (!activeGroup) { 44625 // there is no group active so we do not want to restart loaders 44626 return; 44627 } 44628 44629 if (!activeGroup.playlistLoader) { 44630 // when switching from demuxed audio/video to muxed audio/video (noted by no playlist 44631 // loader for the audio group), we want to do a destructive reset of the main segment 44632 // loader and not restart the audio loaders 44633 mainSegmentLoader.resetEverything(); 44634 return; 44635 } 44636 44637 if (previousActiveLoader === activeGroup.playlistLoader) { 44638 // Nothing has actually changed. This can happen because track change events can fire 44639 // multiple times for a "single" change. One for enabling the new active track, and 44640 // one for disabling the track that was active 44641 startLoaders(activeGroup.playlistLoader, mediaType); 44642 return; 44643 } 44644 44645 if (segmentLoader.track) { 44646 // For WebVTT, set the new text track in the segmentloader 44647 segmentLoader.track(activeTrack); 44648 } 44649 44650 // destructive reset 44651 segmentLoader.resetEverything(); 44652 44653 startLoaders(activeGroup.playlistLoader, mediaType); 44654 }; 44655 }; 44656 44657 var onError = { 44658 /** 44659 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 44660 * an error. 44661 * 44662 * @param {String} type 44663 * MediaGroup type 44664 * @param {Object} settings 44665 * Object containing required information for media groups 44666 * @return {Function} 44667 * Error handler. Logs warning (or error if the playlist is blacklisted) to 44668 * console and switches back to default audio track. 44669 * @function onError.AUDIO 44670 */ 44671 AUDIO: function AUDIO(type, settings) { 44672 return function () { 44673 var segmentLoader = settings.segmentLoaders[type], 44674 mediaType = settings.mediaTypes[type], 44675 blacklistCurrentPlaylist = settings.blacklistCurrentPlaylist; 44676 44677 stopLoaders(segmentLoader, mediaType); 44678 44679 // switch back to default audio track 44680 var activeTrack = mediaType.activeTrack(); 44681 var activeGroup = mediaType.activeGroup(); 44682 var id = (activeGroup.filter(function (group) { 44683 return group.default; 44684 })[0] || activeGroup[0]).id; 44685 var defaultTrack = mediaType.tracks[id]; 44686 44687 if (activeTrack === defaultTrack) { 44688 // Default track encountered an error. All we can do now is blacklist the current 44689 // rendition and hope another will switch audio groups 44690 blacklistCurrentPlaylist({ 44691 message: 'Problem encountered loading the default audio track.' 44692 }); 44693 return; 44694 } 44695 44696 videojs$1.log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.'); 44697 44698 for (var trackId in mediaType.tracks) { 44699 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 44700 } 44701 44702 mediaType.onTrackChanged(); 44703 }; 44704 }, 44705 /** 44706 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 44707 * an error. 44708 * 44709 * @param {String} type 44710 * MediaGroup type 44711 * @param {Object} settings 44712 * Object containing required information for media groups 44713 * @return {Function} 44714 * Error handler. Logs warning to console and disables the active subtitle track 44715 * @function onError.SUBTITLES 44716 */ 44717 SUBTITLES: function SUBTITLES(type, settings) { 44718 return function () { 44719 var segmentLoader = settings.segmentLoaders[type], 44720 mediaType = settings.mediaTypes[type]; 44721 44722 videojs$1.log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.'); 44723 44724 stopLoaders(segmentLoader, mediaType); 44725 44726 var track = mediaType.activeTrack(); 44727 44728 if (track) { 44729 track.mode = 'disabled'; 44730 } 44731 44732 mediaType.onTrackChanged(); 44733 }; 44734 } 44735 }; 44736 44737 var setupListeners = { 44738 /** 44739 * Setup event listeners for audio playlist loader 44740 * 44741 * @param {String} type 44742 * MediaGroup type 44743 * @param {PlaylistLoader|null} playlistLoader 44744 * PlaylistLoader to register listeners on 44745 * @param {Object} settings 44746 * Object containing required information for media groups 44747 * @function setupListeners.AUDIO 44748 */ 44749 AUDIO: function AUDIO(type, playlistLoader, settings) { 44750 if (!playlistLoader) { 44751 // no playlist loader means audio will be muxed with the video 44752 return; 44753 } 44754 44755 var tech = settings.tech, 44756 requestOptions = settings.requestOptions, 44757 segmentLoader = settings.segmentLoaders[type]; 44758 44759 playlistLoader.on('loadedmetadata', function () { 44760 var media = playlistLoader.media(); 44761 44762 segmentLoader.playlist(media, requestOptions); 44763 44764 // if the video is already playing, or if this isn't a live video and preload 44765 // permits, start downloading segments 44766 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 44767 segmentLoader.load(); 44768 } 44769 }); 44770 44771 playlistLoader.on('loadedplaylist', function () { 44772 segmentLoader.playlist(playlistLoader.media(), requestOptions); 44773 44774 // If the player isn't paused, ensure that the segment loader is running 44775 if (!tech.paused()) { 44776 segmentLoader.load(); 44777 } 44778 }); 44779 44780 playlistLoader.on('error', onError[type](type, settings)); 44781 }, 44782 /** 44783 * Setup event listeners for subtitle playlist loader 44784 * 44785 * @param {String} type 44786 * MediaGroup type 44787 * @param {PlaylistLoader|null} playlistLoader 44788 * PlaylistLoader to register listeners on 44789 * @param {Object} settings 44790 * Object containing required information for media groups 44791 * @function setupListeners.SUBTITLES 44792 */ 44793 SUBTITLES: function SUBTITLES(type, playlistLoader, settings) { 44794 var tech = settings.tech, 44795 requestOptions = settings.requestOptions, 44796 segmentLoader = settings.segmentLoaders[type], 44797 mediaType = settings.mediaTypes[type]; 44798 44799 playlistLoader.on('loadedmetadata', function () { 44800 var media = playlistLoader.media(); 44801 44802 segmentLoader.playlist(media, requestOptions); 44803 segmentLoader.track(mediaType.activeTrack()); 44804 44805 // if the video is already playing, or if this isn't a live video and preload 44806 // permits, start downloading segments 44807 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 44808 segmentLoader.load(); 44809 } 44810 }); 44811 44812 playlistLoader.on('loadedplaylist', function () { 44813 segmentLoader.playlist(playlistLoader.media(), requestOptions); 44814 44815 // If the player isn't paused, ensure that the segment loader is running 44816 if (!tech.paused()) { 44817 segmentLoader.load(); 44818 } 44819 }); 44820 44821 playlistLoader.on('error', onError[type](type, settings)); 44822 } 44823 }; 44824 44825 var byGroupId = function byGroupId(type, groupId) { 44826 return function (playlist) { 44827 return playlist.attributes[type] === groupId; 44828 }; 44829 }; 44830 44831 var byResolvedUri = function byResolvedUri(resolvedUri) { 44832 return function (playlist) { 44833 return playlist.resolvedUri === resolvedUri; 44834 }; 44835 }; 44836 44837 var initialize = { 44838 /** 44839 * Setup PlaylistLoaders and AudioTracks for the audio groups 44840 * 44841 * @param {String} type 44842 * MediaGroup type 44843 * @param {Object} settings 44844 * Object containing required information for media groups 44845 * @function initialize.AUDIO 44846 */ 44847 'AUDIO': function AUDIO(type, settings) { 44848 var hls = settings.hls, 44849 sourceType = settings.sourceType, 44850 segmentLoader = settings.segmentLoaders[type], 44851 withCredentials = settings.requestOptions.withCredentials, 44852 _settings$master = settings.master, 44853 mediaGroups = _settings$master.mediaGroups, 44854 playlists = _settings$master.playlists, 44855 _settings$mediaTypes$ = settings.mediaTypes[type], 44856 groups = _settings$mediaTypes$.groups, 44857 tracks = _settings$mediaTypes$.tracks, 44858 masterPlaylistLoader = settings.masterPlaylistLoader; 44859 44860 // force a default if we have none 44861 44862 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 44863 mediaGroups[type] = { main: { default: { default: true } } }; 44864 } 44865 44866 for (var groupId in mediaGroups[type]) { 44867 if (!groups[groupId]) { 44868 groups[groupId] = []; 44869 } 44870 44871 // List of playlists that have an AUDIO attribute value matching the current 44872 // group ID 44873 var groupPlaylists = playlists.filter(byGroupId(type, groupId)); 44874 44875 for (var variantLabel in mediaGroups[type][groupId]) { 44876 var properties = mediaGroups[type][groupId][variantLabel]; 44877 44878 // List of playlists for the current group ID that have a matching uri with 44879 // this alternate audio variant 44880 var matchingPlaylists = groupPlaylists.filter(byResolvedUri(properties.resolvedUri)); 44881 44882 if (matchingPlaylists.length) { 44883 // If there is a playlist that has the same uri as this audio variant, assume 44884 // that the playlist is audio only. We delete the resolvedUri property here 44885 // to prevent a playlist loader from being created so that we don't have 44886 // both the main and audio segment loaders loading the same audio segments 44887 // from the same playlist. 44888 delete properties.resolvedUri; 44889 } 44890 44891 var playlistLoader = void 0; 44892 44893 if (properties.resolvedUri) { 44894 playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, withCredentials); 44895 } else if (properties.playlists && sourceType === 'dash') { 44896 playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, withCredentials, masterPlaylistLoader); 44897 } else { 44898 // no resolvedUri means the audio is muxed with the video when using this 44899 // audio track 44900 playlistLoader = null; 44901 } 44902 44903 properties = videojs$1.mergeOptions({ id: variantLabel, playlistLoader: playlistLoader }, properties); 44904 44905 setupListeners[type](type, properties.playlistLoader, settings); 44906 44907 groups[groupId].push(properties); 44908 44909 if (typeof tracks[variantLabel] === 'undefined') { 44910 var track = new videojs$1.AudioTrack({ 44911 id: variantLabel, 44912 kind: audioTrackKind_(properties), 44913 enabled: false, 44914 language: properties.language, 44915 default: properties.default, 44916 label: variantLabel 44917 }); 44918 44919 tracks[variantLabel] = track; 44920 } 44921 } 44922 } 44923 44924 // setup single error event handler for the segment loader 44925 segmentLoader.on('error', onError[type](type, settings)); 44926 }, 44927 /** 44928 * Setup PlaylistLoaders and TextTracks for the subtitle groups 44929 * 44930 * @param {String} type 44931 * MediaGroup type 44932 * @param {Object} settings 44933 * Object containing required information for media groups 44934 * @function initialize.SUBTITLES 44935 */ 44936 'SUBTITLES': function SUBTITLES(type, settings) { 44937 var tech = settings.tech, 44938 hls = settings.hls, 44939 sourceType = settings.sourceType, 44940 segmentLoader = settings.segmentLoaders[type], 44941 withCredentials = settings.requestOptions.withCredentials, 44942 mediaGroups = settings.master.mediaGroups, 44943 _settings$mediaTypes$2 = settings.mediaTypes[type], 44944 groups = _settings$mediaTypes$2.groups, 44945 tracks = _settings$mediaTypes$2.tracks, 44946 masterPlaylistLoader = settings.masterPlaylistLoader; 44947 44948 for (var groupId in mediaGroups[type]) { 44949 if (!groups[groupId]) { 44950 groups[groupId] = []; 44951 } 44952 44953 for (var variantLabel in mediaGroups[type][groupId]) { 44954 if (mediaGroups[type][groupId][variantLabel].forced) { 44955 // Subtitle playlists with the forced attribute are not selectable in Safari. 44956 // According to Apple's HLS Authoring Specification: 44957 // If content has forced subtitles and regular subtitles in a given language, 44958 // the regular subtitles track in that language MUST contain both the forced 44959 // subtitles and the regular subtitles for that language. 44960 // Because of this requirement and that Safari does not add forced subtitles, 44961 // forced subtitles are skipped here to maintain consistent experience across 44962 // all platforms 44963 continue; 44964 } 44965 44966 var properties = mediaGroups[type][groupId][variantLabel]; 44967 44968 var playlistLoader = void 0; 44969 44970 if (sourceType === 'hls') { 44971 playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, withCredentials); 44972 } else if (sourceType === 'dash') { 44973 playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, withCredentials, masterPlaylistLoader); 44974 } 44975 44976 properties = videojs$1.mergeOptions({ 44977 id: variantLabel, 44978 playlistLoader: playlistLoader 44979 }, properties); 44980 44981 setupListeners[type](type, properties.playlistLoader, settings); 44982 44983 groups[groupId].push(properties); 44984 44985 if (typeof tracks[variantLabel] === 'undefined') { 44986 var track = tech.addRemoteTextTrack({ 44987 id: variantLabel, 44988 kind: 'subtitles', 44989 enabled: false, 44990 language: properties.language, 44991 label: variantLabel 44992 }, false).track; 44993 44994 tracks[variantLabel] = track; 44995 } 44996 } 44997 } 44998 44999 // setup single error event handler for the segment loader 45000 segmentLoader.on('error', onError[type](type, settings)); 45001 }, 45002 /** 45003 * Setup TextTracks for the closed-caption groups 45004 * 45005 * @param {String} type 45006 * MediaGroup type 45007 * @param {Object} settings 45008 * Object containing required information for media groups 45009 * @function initialize['CLOSED-CAPTIONS'] 45010 */ 45011 'CLOSED-CAPTIONS': function CLOSEDCAPTIONS(type, settings) { 45012 var tech = settings.tech, 45013 mediaGroups = settings.master.mediaGroups, 45014 _settings$mediaTypes$3 = settings.mediaTypes[type], 45015 groups = _settings$mediaTypes$3.groups, 45016 tracks = _settings$mediaTypes$3.tracks; 45017 45018 for (var groupId in mediaGroups[type]) { 45019 if (!groups[groupId]) { 45020 groups[groupId] = []; 45021 } 45022 45023 for (var variantLabel in mediaGroups[type][groupId]) { 45024 var properties = mediaGroups[type][groupId][variantLabel]; 45025 45026 // We only support CEA608 captions for now, so ignore anything that 45027 // doesn't use a CCx INSTREAM-ID 45028 if (!properties.instreamId.match(/CC\d/)) { 45029 continue; 45030 } 45031 45032 // No PlaylistLoader is required for Closed-Captions because the captions are 45033 // embedded within the video stream 45034 groups[groupId].push(videojs$1.mergeOptions({ id: variantLabel }, properties)); 45035 45036 if (typeof tracks[variantLabel] === 'undefined') { 45037 var track = tech.addRemoteTextTrack({ 45038 id: properties.instreamId, 45039 kind: 'captions', 45040 enabled: false, 45041 language: properties.language, 45042 label: variantLabel 45043 }, false).track; 45044 45045 tracks[variantLabel] = track; 45046 } 45047 } 45048 } 45049 } 45050 }; 45051 45052 /** 45053 * Returns a function used to get the active group of the provided type 45054 * 45055 * @param {String} type 45056 * MediaGroup type 45057 * @param {Object} settings 45058 * Object containing required information for media groups 45059 * @return {Function} 45060 * Function that returns the active media group for the provided type. Takes an 45061 * optional parameter {TextTrack} track. If no track is provided, a list of all 45062 * variants in the group, otherwise the variant corresponding to the provided 45063 * track is returned. 45064 * @function activeGroup 45065 */ 45066 var activeGroup = function activeGroup(type, settings) { 45067 return function (track) { 45068 var masterPlaylistLoader = settings.masterPlaylistLoader, 45069 groups = settings.mediaTypes[type].groups; 45070 45071 var media = masterPlaylistLoader.media(); 45072 45073 if (!media) { 45074 return null; 45075 } 45076 45077 var variants = null; 45078 45079 if (media.attributes[type]) { 45080 variants = groups[media.attributes[type]]; 45081 } 45082 45083 variants = variants || groups.main; 45084 45085 if (typeof track === 'undefined') { 45086 return variants; 45087 } 45088 45089 if (track === null) { 45090 // An active track was specified so a corresponding group is expected. track === null 45091 // means no track is currently active so there is no corresponding group 45092 return null; 45093 } 45094 45095 return variants.filter(function (props) { 45096 return props.id === track.id; 45097 })[0] || null; 45098 }; 45099 }; 45100 45101 var activeTrack = { 45102 /** 45103 * Returns a function used to get the active track of type provided 45104 * 45105 * @param {String} type 45106 * MediaGroup type 45107 * @param {Object} settings 45108 * Object containing required information for media groups 45109 * @return {Function} 45110 * Function that returns the active media track for the provided type. Returns 45111 * null if no track is active 45112 * @function activeTrack.AUDIO 45113 */ 45114 AUDIO: function AUDIO(type, settings) { 45115 return function () { 45116 var tracks = settings.mediaTypes[type].tracks; 45117 45118 for (var id in tracks) { 45119 if (tracks[id].enabled) { 45120 return tracks[id]; 45121 } 45122 } 45123 45124 return null; 45125 }; 45126 }, 45127 /** 45128 * Returns a function used to get the active track of type provided 45129 * 45130 * @param {String} type 45131 * MediaGroup type 45132 * @param {Object} settings 45133 * Object containing required information for media groups 45134 * @return {Function} 45135 * Function that returns the active media track for the provided type. Returns 45136 * null if no track is active 45137 * @function activeTrack.SUBTITLES 45138 */ 45139 SUBTITLES: function SUBTITLES(type, settings) { 45140 return function () { 45141 var tracks = settings.mediaTypes[type].tracks; 45142 45143 for (var id in tracks) { 45144 if (tracks[id].mode === 'showing') { 45145 return tracks[id]; 45146 } 45147 } 45148 45149 return null; 45150 }; 45151 } 45152 }; 45153 45154 /** 45155 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 45156 * Closed-Captions) specified in the master manifest. 45157 * 45158 * @param {Object} settings 45159 * Object containing required information for setting up the media groups 45160 * @param {SegmentLoader} settings.segmentLoaders.AUDIO 45161 * Audio segment loader 45162 * @param {SegmentLoader} settings.segmentLoaders.SUBTITLES 45163 * Subtitle segment loader 45164 * @param {SegmentLoader} settings.segmentLoaders.main 45165 * Main segment loader 45166 * @param {Tech} settings.tech 45167 * The tech of the player 45168 * @param {Object} settings.requestOptions 45169 * XHR request options used by the segment loaders 45170 * @param {PlaylistLoader} settings.masterPlaylistLoader 45171 * PlaylistLoader for the master source 45172 * @param {HlsHandler} settings.hls 45173 * HLS SourceHandler 45174 * @param {Object} settings.master 45175 * The parsed master manifest 45176 * @param {Object} settings.mediaTypes 45177 * Object to store the loaders, tracks, and utility methods for each media type 45178 * @param {Function} settings.blacklistCurrentPlaylist 45179 * Blacklists the current rendition and forces a rendition switch. 45180 * @function setupMediaGroups 45181 */ 45182 var setupMediaGroups = function setupMediaGroups(settings) { 45183 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) { 45184 initialize[type](type, settings); 45185 }); 45186 45187 var mediaTypes = settings.mediaTypes, 45188 masterPlaylistLoader = settings.masterPlaylistLoader, 45189 tech = settings.tech, 45190 hls = settings.hls; 45191 45192 // setup active group and track getters and change event handlers 45193 45194 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 45195 mediaTypes[type].activeGroup = activeGroup(type, settings); 45196 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 45197 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 45198 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 45199 }); 45200 45201 // DO NOT enable the default subtitle or caption track. 45202 // DO enable the default audio track 45203 var audioGroup = mediaTypes.AUDIO.activeGroup(); 45204 var groupId = (audioGroup.filter(function (group) { 45205 return group.default; 45206 })[0] || audioGroup[0]).id; 45207 45208 mediaTypes.AUDIO.tracks[groupId].enabled = true; 45209 mediaTypes.AUDIO.onTrackChanged(); 45210 45211 masterPlaylistLoader.on('mediachange', function () { 45212 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 45213 return mediaTypes[type].onGroupChanged(); 45214 }); 45215 }); 45216 45217 // custom audio track change event handler for usage event 45218 var onAudioTrackChanged = function onAudioTrackChanged() { 45219 mediaTypes.AUDIO.onTrackChanged(); 45220 tech.trigger({ type: 'usage', name: 'hls-audio-change' }); 45221 }; 45222 45223 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 45224 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 45225 45226 hls.on('dispose', function () { 45227 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 45228 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 45229 }); 45230 45231 // clear existing audio tracks and add the ones we just created 45232 tech.clearTracks('audio'); 45233 45234 for (var id in mediaTypes.AUDIO.tracks) { 45235 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 45236 } 45237 }; 45238 45239 /** 45240 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 45241 * media type 45242 * 45243 * @return {Object} 45244 * Object to store the loaders, tracks, and utility methods for each media type 45245 * @function createMediaTypes 45246 */ 45247 var createMediaTypes = function createMediaTypes() { 45248 var mediaTypes = {}; 45249 45250 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) { 45251 mediaTypes[type] = { 45252 groups: {}, 45253 tracks: {}, 45254 activePlaylistLoader: null, 45255 activeGroup: noop$1, 45256 activeTrack: noop$1, 45257 onGroupChanged: noop$1, 45258 onTrackChanged: noop$1 45259 }; 45260 }); 45261 45262 return mediaTypes; 45263 }; 45264 45265 /** 45266 * @file master-playlist-controller.js 45267 */ 45268 45269 var ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2; 45270 45271 var Hls = void 0; 45272 45273 // SegmentLoader stats that need to have each loader's 45274 // values summed to calculate the final value 45275 var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred']; 45276 var sumLoaderStat = function sumLoaderStat(stat) { 45277 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 45278 }; 45279 45280 /** 45281 * the master playlist controller controller all interactons 45282 * between playlists and segmentloaders. At this time this mainly 45283 * involves a master playlist and a series of audio playlists 45284 * if they are available 45285 * 45286 * @class MasterPlaylistController 45287 * @extends videojs.EventTarget 45288 */ 45289 var MasterPlaylistController = function (_videojs$EventTarget) { 45290 inherits$3(MasterPlaylistController, _videojs$EventTarget); 45291 45292 function MasterPlaylistController(options) { 45293 classCallCheck$3(this, MasterPlaylistController); 45294 45295 var _this = possibleConstructorReturn$3(this, (MasterPlaylistController.__proto__ || Object.getPrototypeOf(MasterPlaylistController)).call(this)); 45296 45297 var url = options.url, 45298 withCredentials = options.withCredentials, 45299 tech = options.tech, 45300 bandwidth = options.bandwidth, 45301 externHls = options.externHls, 45302 useCueTags = options.useCueTags, 45303 blacklistDuration = options.blacklistDuration, 45304 enableLowInitialPlaylist = options.enableLowInitialPlaylist, 45305 sourceType = options.sourceType; 45306 45307 if (!url) { 45308 throw new Error('A non-empty playlist URL is required'); 45309 } 45310 45311 Hls = externHls; 45312 45313 _this.withCredentials = withCredentials; 45314 _this.tech_ = tech; 45315 _this.hls_ = tech.hls; 45316 _this.sourceType_ = sourceType; 45317 _this.useCueTags_ = useCueTags; 45318 _this.blacklistDuration = blacklistDuration; 45319 _this.enableLowInitialPlaylist = enableLowInitialPlaylist; 45320 if (_this.useCueTags_) { 45321 _this.cueTagsTrack_ = _this.tech_.addTextTrack('metadata', 'ad-cues'); 45322 _this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 45323 } 45324 45325 _this.requestOptions_ = { 45326 withCredentials: _this.withCredentials, 45327 timeout: null 45328 }; 45329 45330 _this.mediaTypes_ = createMediaTypes(); 45331 45332 _this.mediaSource = new videojs$1.MediaSource(); 45333 45334 // load the media source into the player 45335 _this.mediaSource.addEventListener('sourceopen', _this.handleSourceOpen_.bind(_this)); 45336 45337 _this.seekable_ = videojs$1.createTimeRanges(); 45338 _this.hasPlayed_ = function () { 45339 return false; 45340 }; 45341 45342 _this.syncController_ = new SyncController(options); 45343 _this.segmentMetadataTrack_ = tech.addRemoteTextTrack({ 45344 kind: 'metadata', 45345 label: 'segment-metadata' 45346 }, false).track; 45347 45348 _this.decrypter_ = new Decrypter$1(); 45349 45350 var segmentLoaderSettings = { 45351 hls: _this.hls_, 45352 mediaSource: _this.mediaSource, 45353 currentTime: _this.tech_.currentTime.bind(_this.tech_), 45354 seekable: function seekable$$1() { 45355 return _this.seekable(); 45356 }, 45357 seeking: function seeking() { 45358 return _this.tech_.seeking(); 45359 }, 45360 duration: function duration$$1() { 45361 return _this.mediaSource.duration; 45362 }, 45363 hasPlayed: function hasPlayed() { 45364 return _this.hasPlayed_(); 45365 }, 45366 goalBufferLength: function goalBufferLength() { 45367 return _this.goalBufferLength(); 45368 }, 45369 bandwidth: bandwidth, 45370 syncController: _this.syncController_, 45371 decrypter: _this.decrypter_, 45372 sourceType: _this.sourceType_ 45373 }; 45374 45375 _this.masterPlaylistLoader_ = _this.sourceType_ === 'dash' ? new DashPlaylistLoader(url, _this.hls_, _this.withCredentials) : new PlaylistLoader(url, _this.hls_, _this.withCredentials); 45376 _this.setupMasterPlaylistLoaderListeners_(); 45377 45378 // setup segment loaders 45379 // combined audio/video or just video when alternate audio track is selected 45380 _this.mainSegmentLoader_ = new SegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 45381 segmentMetadataTrack: _this.segmentMetadataTrack_, 45382 loaderType: 'main' 45383 }), options); 45384 45385 // alternate audio track 45386 _this.audioSegmentLoader_ = new SegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 45387 loaderType: 'audio' 45388 }), options); 45389 45390 _this.subtitleSegmentLoader_ = new VTTSegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 45391 loaderType: 'vtt' 45392 }), options); 45393 45394 _this.setupSegmentLoaderListeners_(); 45395 45396 // Create SegmentLoader stat-getters 45397 loaderStats.forEach(function (stat) { 45398 _this[stat + '_'] = sumLoaderStat.bind(_this, stat); 45399 }); 45400 45401 _this.logger_ = logger('MPC'); 45402 45403 _this.masterPlaylistLoader_.load(); 45404 return _this; 45405 } 45406 45407 /** 45408 * Register event handlers on the master playlist loader. A helper 45409 * function for construction time. 45410 * 45411 * @private 45412 */ 45413 45414 createClass$2(MasterPlaylistController, [{ 45415 key: 'setupMasterPlaylistLoaderListeners_', 45416 value: function setupMasterPlaylistLoaderListeners_() { 45417 var _this2 = this; 45418 45419 this.masterPlaylistLoader_.on('loadedmetadata', function () { 45420 var media = _this2.masterPlaylistLoader_.media(); 45421 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000; 45422 45423 // If we don't have any more available playlists, we don't want to 45424 // timeout the request. 45425 if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) { 45426 _this2.requestOptions_.timeout = 0; 45427 } else { 45428 _this2.requestOptions_.timeout = requestTimeout; 45429 } 45430 45431 // if this isn't a live video and preload permits, start 45432 // downloading segments 45433 if (media.endList && _this2.tech_.preload() !== 'none') { 45434 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); 45435 _this2.mainSegmentLoader_.load(); 45436 } 45437 45438 setupMediaGroups({ 45439 sourceType: _this2.sourceType_, 45440 segmentLoaders: { 45441 AUDIO: _this2.audioSegmentLoader_, 45442 SUBTITLES: _this2.subtitleSegmentLoader_, 45443 main: _this2.mainSegmentLoader_ 45444 }, 45445 tech: _this2.tech_, 45446 requestOptions: _this2.requestOptions_, 45447 masterPlaylistLoader: _this2.masterPlaylistLoader_, 45448 hls: _this2.hls_, 45449 master: _this2.master(), 45450 mediaTypes: _this2.mediaTypes_, 45451 blacklistCurrentPlaylist: _this2.blacklistCurrentPlaylist.bind(_this2) 45452 }); 45453 45454 _this2.triggerPresenceUsage_(_this2.master(), media); 45455 45456 try { 45457 _this2.setupSourceBuffers_(); 45458 } catch (e) { 45459 videojs$1.log.warn('Failed to create SourceBuffers', e); 45460 return _this2.mediaSource.endOfStream('decode'); 45461 } 45462 _this2.setupFirstPlay(); 45463 45464 _this2.trigger('selectedinitialmedia'); 45465 }); 45466 45467 this.masterPlaylistLoader_.on('loadedplaylist', function () { 45468 var updatedPlaylist = _this2.masterPlaylistLoader_.media(); 45469 45470 if (!updatedPlaylist) { 45471 // blacklist any variants that are not supported by the browser before selecting 45472 // an initial media as the playlist selectors do not consider browser support 45473 _this2.excludeUnsupportedVariants_(); 45474 45475 var selectedMedia = void 0; 45476 45477 if (_this2.enableLowInitialPlaylist) { 45478 selectedMedia = _this2.selectInitialPlaylist(); 45479 } 45480 45481 if (!selectedMedia) { 45482 selectedMedia = _this2.selectPlaylist(); 45483 } 45484 45485 _this2.initialMedia_ = selectedMedia; 45486 _this2.masterPlaylistLoader_.media(_this2.initialMedia_); 45487 return; 45488 } 45489 45490 if (_this2.useCueTags_) { 45491 _this2.updateAdCues_(updatedPlaylist); 45492 } 45493 45494 // TODO: Create a new event on the PlaylistLoader that signals 45495 // that the segments have changed in some way and use that to 45496 // update the SegmentLoader instead of doing it twice here and 45497 // on `mediachange` 45498 _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_); 45499 _this2.updateDuration(); 45500 45501 // If the player isn't paused, ensure that the segment loader is running, 45502 // as it is possible that it was temporarily stopped while waiting for 45503 // a playlist (e.g., in case the playlist errored and we re-requested it). 45504 if (!_this2.tech_.paused()) { 45505 _this2.mainSegmentLoader_.load(); 45506 } 45507 45508 if (!updatedPlaylist.endList) { 45509 var addSeekableRange = function addSeekableRange() { 45510 var seekable$$1 = _this2.seekable(); 45511 45512 if (seekable$$1.length !== 0) { 45513 _this2.mediaSource.addSeekableRange_(seekable$$1.start(0), seekable$$1.end(0)); 45514 } 45515 }; 45516 45517 if (_this2.duration() !== Infinity) { 45518 var onDurationchange = function onDurationchange() { 45519 if (_this2.duration() === Infinity) { 45520 addSeekableRange(); 45521 } else { 45522 _this2.tech_.one('durationchange', onDurationchange); 45523 } 45524 }; 45525 45526 _this2.tech_.one('durationchange', onDurationchange); 45527 } else { 45528 addSeekableRange(); 45529 } 45530 } 45531 }); 45532 45533 this.masterPlaylistLoader_.on('error', function () { 45534 _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error); 45535 }); 45536 45537 this.masterPlaylistLoader_.on('mediachanging', function () { 45538 _this2.mainSegmentLoader_.abort(); 45539 _this2.mainSegmentLoader_.pause(); 45540 }); 45541 45542 this.masterPlaylistLoader_.on('mediachange', function () { 45543 var media = _this2.masterPlaylistLoader_.media(); 45544 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000; 45545 45546 // If we don't have any more available playlists, we don't want to 45547 // timeout the request. 45548 if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) { 45549 _this2.requestOptions_.timeout = 0; 45550 } else { 45551 _this2.requestOptions_.timeout = requestTimeout; 45552 } 45553 45554 // TODO: Create a new event on the PlaylistLoader that signals 45555 // that the segments have changed in some way and use that to 45556 // update the SegmentLoader instead of doing it twice here and 45557 // on `loadedplaylist` 45558 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); 45559 _this2.mainSegmentLoader_.load(); 45560 45561 _this2.tech_.trigger({ 45562 type: 'mediachange', 45563 bubbles: true 45564 }); 45565 }); 45566 45567 this.masterPlaylistLoader_.on('playlistunchanged', function () { 45568 var updatedPlaylist = _this2.masterPlaylistLoader_.media(); 45569 var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist); 45570 45571 if (playlistOutdated) { 45572 // Playlist has stopped updating and we're stuck at its end. Try to 45573 // blacklist it and switch to another playlist in the hope that that 45574 // one is updating (and give the player a chance to re-adjust to the 45575 // safe live point). 45576 _this2.blacklistCurrentPlaylist({ 45577 message: 'Playlist no longer updating.' 45578 }); 45579 // useful for monitoring QoS 45580 _this2.tech_.trigger('playliststuck'); 45581 } 45582 }); 45583 45584 this.masterPlaylistLoader_.on('renditiondisabled', function () { 45585 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-disabled' }); 45586 }); 45587 this.masterPlaylistLoader_.on('renditionenabled', function () { 45588 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-enabled' }); 45589 }); 45590 } 45591 45592 /** 45593 * A helper function for triggerring presence usage events once per source 45594 * 45595 * @private 45596 */ 45597 45598 }, { 45599 key: 'triggerPresenceUsage_', 45600 value: function triggerPresenceUsage_(master, media) { 45601 var mediaGroups = master.mediaGroups || {}; 45602 var defaultDemuxed = true; 45603 var audioGroupKeys = Object.keys(mediaGroups.AUDIO); 45604 45605 for (var mediaGroup in mediaGroups.AUDIO) { 45606 for (var label in mediaGroups.AUDIO[mediaGroup]) { 45607 var properties = mediaGroups.AUDIO[mediaGroup][label]; 45608 45609 if (!properties.uri) { 45610 defaultDemuxed = false; 45611 } 45612 } 45613 } 45614 45615 if (defaultDemuxed) { 45616 this.tech_.trigger({ type: 'usage', name: 'hls-demuxed' }); 45617 } 45618 45619 if (Object.keys(mediaGroups.SUBTITLES).length) { 45620 this.tech_.trigger({ type: 'usage', name: 'hls-webvtt' }); 45621 } 45622 45623 if (Hls.Playlist.isAes(media)) { 45624 this.tech_.trigger({ type: 'usage', name: 'hls-aes' }); 45625 } 45626 45627 if (Hls.Playlist.isFmp4(media)) { 45628 this.tech_.trigger({ type: 'usage', name: 'hls-fmp4' }); 45629 } 45630 45631 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 45632 this.tech_.trigger({ type: 'usage', name: 'hls-alternate-audio' }); 45633 } 45634 45635 if (this.useCueTags_) { 45636 this.tech_.trigger({ type: 'usage', name: 'hls-playlist-cue-tags' }); 45637 } 45638 } 45639 /** 45640 * Register event handlers on the segment loaders. A helper function 45641 * for construction time. 45642 * 45643 * @private 45644 */ 45645 45646 }, { 45647 key: 'setupSegmentLoaderListeners_', 45648 value: function setupSegmentLoaderListeners_() { 45649 var _this3 = this; 45650 45651 this.mainSegmentLoader_.on('bandwidthupdate', function () { 45652 var nextPlaylist = _this3.selectPlaylist(); 45653 var currentPlaylist = _this3.masterPlaylistLoader_.media(); 45654 var buffered = _this3.tech_.buffered(); 45655 var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0; 45656 45657 var bufferLowWaterLine = _this3.bufferLowWaterLine(); 45658 45659 // If the playlist is live, then we want to not take low water line into account. 45660 // This is because in LIVE, the player plays 3 segments from the end of the 45661 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 45662 // in those segments, a viewer will never experience a rendition upswitch. 45663 if (!currentPlaylist.endList || 45664 // For the same reason as LIVE, we ignore the low water line when the VOD 45665 // duration is below the max potential low water line 45666 _this3.duration() < Config.MAX_BUFFER_LOW_WATER_LINE || 45667 // we want to switch down to lower resolutions quickly to continue playback, but 45668 nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH || 45669 // ensure we have some buffer before we switch up to prevent us running out of 45670 // buffer while loading a higher rendition. 45671 forwardBuffer >= bufferLowWaterLine) { 45672 _this3.masterPlaylistLoader_.media(nextPlaylist); 45673 } 45674 45675 _this3.tech_.trigger('bandwidthupdate'); 45676 }); 45677 this.mainSegmentLoader_.on('progress', function () { 45678 _this3.trigger('progress'); 45679 }); 45680 45681 this.mainSegmentLoader_.on('error', function () { 45682 _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error()); 45683 }); 45684 45685 this.mainSegmentLoader_.on('syncinfoupdate', function () { 45686 _this3.onSyncInfoUpdate_(); 45687 }); 45688 45689 this.mainSegmentLoader_.on('timestampoffset', function () { 45690 _this3.tech_.trigger({ type: 'usage', name: 'hls-timestamp-offset' }); 45691 }); 45692 this.audioSegmentLoader_.on('syncinfoupdate', function () { 45693 _this3.onSyncInfoUpdate_(); 45694 }); 45695 45696 this.mainSegmentLoader_.on('ended', function () { 45697 _this3.onEndOfStream(); 45698 }); 45699 45700 this.mainSegmentLoader_.on('earlyabort', function () { 45701 _this3.blacklistCurrentPlaylist({ 45702 message: 'Aborted early because there isn\'t enough bandwidth to complete the ' + 'request without rebuffering.' 45703 }, ABORT_EARLY_BLACKLIST_SECONDS); 45704 }); 45705 45706 this.mainSegmentLoader_.on('reseteverything', function () { 45707 // If playing an MTS stream, a videojs.MediaSource is listening for 45708 // hls-reset to reset caption parsing state in the transmuxer 45709 _this3.tech_.trigger('hls-reset'); 45710 }); 45711 45712 this.mainSegmentLoader_.on('segmenttimemapping', function (event) { 45713 // If playing an MTS stream in html, a videojs.MediaSource is listening for 45714 // hls-segment-time-mapping update its internal mapping of stream to display time 45715 _this3.tech_.trigger({ 45716 type: 'hls-segment-time-mapping', 45717 mapping: event.mapping 45718 }); 45719 }); 45720 45721 this.audioSegmentLoader_.on('ended', function () { 45722 _this3.onEndOfStream(); 45723 }); 45724 } 45725 }, { 45726 key: 'mediaSecondsLoaded_', 45727 value: function mediaSecondsLoaded_() { 45728 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded); 45729 } 45730 45731 /** 45732 * Call load on our SegmentLoaders 45733 */ 45734 45735 }, { 45736 key: 'load', 45737 value: function load() { 45738 this.mainSegmentLoader_.load(); 45739 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 45740 this.audioSegmentLoader_.load(); 45741 } 45742 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 45743 this.subtitleSegmentLoader_.load(); 45744 } 45745 } 45746 45747 /** 45748 * Re-tune playback quality level for the current player 45749 * conditions. This method may perform destructive actions, like 45750 * removing already buffered content, to readjust the currently 45751 * active playlist quickly. 45752 * 45753 * @private 45754 */ 45755 45756 }, { 45757 key: 'fastQualityChange_', 45758 value: function fastQualityChange_() { 45759 var media = this.selectPlaylist(); 45760 45761 if (media !== this.masterPlaylistLoader_.media()) { 45762 this.masterPlaylistLoader_.media(media); 45763 45764 this.mainSegmentLoader_.resetLoader(); 45765 // don't need to reset audio as it is reset when media changes 45766 } 45767 } 45768 45769 /** 45770 * Begin playback. 45771 */ 45772 45773 }, { 45774 key: 'play', 45775 value: function play() { 45776 if (this.setupFirstPlay()) { 45777 return; 45778 } 45779 45780 if (this.tech_.ended()) { 45781 this.tech_.setCurrentTime(0); 45782 } 45783 45784 if (this.hasPlayed_()) { 45785 this.load(); 45786 } 45787 45788 var seekable$$1 = this.tech_.seekable(); 45789 45790 // if the viewer has paused and we fell out of the live window, 45791 // seek forward to the live point 45792 if (this.tech_.duration() === Infinity) { 45793 if (this.tech_.currentTime() < seekable$$1.start(0)) { 45794 return this.tech_.setCurrentTime(seekable$$1.end(seekable$$1.length - 1)); 45795 } 45796 } 45797 } 45798 45799 /** 45800 * Seek to the latest media position if this is a live video and the 45801 * player and video are loaded and initialized. 45802 */ 45803 45804 }, { 45805 key: 'setupFirstPlay', 45806 value: function setupFirstPlay() { 45807 var _this4 = this; 45808 45809 var media = this.masterPlaylistLoader_.media(); 45810 45811 // Check that everything is ready to begin buffering for the first call to play 45812 // If 1) there is no active media 45813 // 2) the player is paused 45814 // 3) the first play has already been setup 45815 // then exit early 45816 if (!media || this.tech_.paused() || this.hasPlayed_()) { 45817 return false; 45818 } 45819 45820 // when the video is a live stream 45821 if (!media.endList) { 45822 var seekable$$1 = this.seekable(); 45823 45824 if (!seekable$$1.length) { 45825 // without a seekable range, the player cannot seek to begin buffering at the live 45826 // point 45827 return false; 45828 } 45829 45830 if (videojs$1.browser.IE_VERSION && this.tech_.readyState() === 0) { 45831 // IE11 throws an InvalidStateError if you try to set currentTime while the 45832 // readyState is 0, so it must be delayed until the tech fires loadedmetadata. 45833 this.tech_.one('loadedmetadata', function () { 45834 _this4.trigger('firstplay'); 45835 _this4.tech_.setCurrentTime(seekable$$1.end(0)); 45836 _this4.hasPlayed_ = function () { 45837 return true; 45838 }; 45839 }); 45840 45841 return false; 45842 } 45843 45844 // trigger firstplay to inform the source handler to ignore the next seek event 45845 this.trigger('firstplay'); 45846 // seek to the live point 45847 this.tech_.setCurrentTime(seekable$$1.end(0)); 45848 } 45849 45850 this.hasPlayed_ = function () { 45851 return true; 45852 }; 45853 // we can begin loading now that everything is ready 45854 this.load(); 45855 return true; 45856 } 45857 45858 /** 45859 * handle the sourceopen event on the MediaSource 45860 * 45861 * @private 45862 */ 45863 45864 }, { 45865 key: 'handleSourceOpen_', 45866 value: function handleSourceOpen_() { 45867 // Only attempt to create the source buffer if none already exist. 45868 // handleSourceOpen is also called when we are "re-opening" a source buffer 45869 // after `endOfStream` has been called (in response to a seek for instance) 45870 try { 45871 this.setupSourceBuffers_(); 45872 } catch (e) { 45873 videojs$1.log.warn('Failed to create Source Buffers', e); 45874 return this.mediaSource.endOfStream('decode'); 45875 } 45876 45877 // if autoplay is enabled, begin playback. This is duplicative of 45878 // code in video.js but is required because play() must be invoked 45879 // *after* the media source has opened. 45880 if (this.tech_.autoplay()) { 45881 var playPromise = this.tech_.play(); 45882 45883 // Catch/silence error when a pause interrupts a play request 45884 // on browsers which return a promise 45885 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 45886 playPromise.then(null, function (e) {}); 45887 } 45888 } 45889 45890 this.trigger('sourceopen'); 45891 } 45892 45893 /** 45894 * Calls endOfStream on the media source when all active stream types have called 45895 * endOfStream 45896 * 45897 * @param {string} streamType 45898 * Stream type of the segment loader that called endOfStream 45899 * @private 45900 */ 45901 45902 }, { 45903 key: 'onEndOfStream', 45904 value: function onEndOfStream() { 45905 var isEndOfStream = this.mainSegmentLoader_.ended_; 45906 45907 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 45908 // if the audio playlist loader exists, then alternate audio is active 45909 if (!this.mainSegmentLoader_.startingMedia_ || this.mainSegmentLoader_.startingMedia_.containsVideo) { 45910 // if we do not know if the main segment loader contains video yet or if we 45911 // definitively know the main segment loader contains video, then we need to wait 45912 // for both main and audio segment loaders to call endOfStream 45913 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 45914 } else { 45915 // otherwise just rely on the audio loader 45916 isEndOfStream = this.audioSegmentLoader_.ended_; 45917 } 45918 } 45919 45920 if (isEndOfStream) { 45921 this.mediaSource.endOfStream(); 45922 } 45923 } 45924 45925 /** 45926 * Check if a playlist has stopped being updated 45927 * @param {Object} playlist the media playlist object 45928 * @return {boolean} whether the playlist has stopped being updated or not 45929 */ 45930 45931 }, { 45932 key: 'stuckAtPlaylistEnd_', 45933 value: function stuckAtPlaylistEnd_(playlist) { 45934 var seekable$$1 = this.seekable(); 45935 45936 if (!seekable$$1.length) { 45937 // playlist doesn't have enough information to determine whether we are stuck 45938 return false; 45939 } 45940 45941 var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration); 45942 45943 if (expired === null) { 45944 return false; 45945 } 45946 45947 // does not use the safe live end to calculate playlist end, since we 45948 // don't want to say we are stuck while there is still content 45949 var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired); 45950 var currentTime = this.tech_.currentTime(); 45951 var buffered = this.tech_.buffered(); 45952 45953 if (!buffered.length) { 45954 // return true if the playhead reached the absolute end of the playlist 45955 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 45956 } 45957 var bufferedEnd = buffered.end(buffered.length - 1); 45958 45959 // return true if there is too little buffer left and buffer has reached absolute 45960 // end of playlist 45961 return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA; 45962 } 45963 45964 /** 45965 * Blacklists a playlist when an error occurs for a set amount of time 45966 * making it unavailable for selection by the rendition selection algorithm 45967 * and then forces a new playlist (rendition) selection. 45968 * 45969 * @param {Object=} error an optional error that may include the playlist 45970 * to blacklist 45971 * @param {Number=} blacklistDuration an optional number of seconds to blacklist the 45972 * playlist 45973 */ 45974 45975 }, { 45976 key: 'blacklistCurrentPlaylist', 45977 value: function blacklistCurrentPlaylist() { 45978 var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 45979 var blacklistDuration = arguments[1]; 45980 45981 var currentPlaylist = void 0; 45982 var nextPlaylist = void 0; 45983 45984 // If the `error` was generated by the playlist loader, it will contain 45985 // the playlist we were trying to load (but failed) and that should be 45986 // blacklisted instead of the currently selected playlist which is likely 45987 // out-of-date in this scenario 45988 currentPlaylist = error.playlist || this.masterPlaylistLoader_.media(); 45989 45990 blacklistDuration = blacklistDuration || error.blacklistDuration || this.blacklistDuration; 45991 45992 // If there is no current playlist, then an error occurred while we were 45993 // trying to load the master OR while we were disposing of the tech 45994 if (!currentPlaylist) { 45995 this.error = error; 45996 45997 try { 45998 return this.mediaSource.endOfStream('network'); 45999 } catch (e) { 46000 return this.trigger('error'); 46001 } 46002 } 46003 46004 var isFinalRendition = this.masterPlaylistLoader_.master.playlists.filter(isEnabled).length === 1; 46005 46006 if (isFinalRendition) { 46007 // Never blacklisting this playlist because it's final rendition 46008 videojs$1.log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the final playlist.'); 46009 46010 this.tech_.trigger('retryplaylist'); 46011 return this.masterPlaylistLoader_.load(isFinalRendition); 46012 } 46013 // Blacklist this playlist 46014 currentPlaylist.excludeUntil = Date.now() + blacklistDuration * 1000; 46015 this.tech_.trigger('blacklistplaylist'); 46016 this.tech_.trigger({ type: 'usage', name: 'hls-rendition-blacklisted' }); 46017 46018 // Select a new playlist 46019 nextPlaylist = this.selectPlaylist(); 46020 videojs$1.log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.'); 46021 46022 return this.masterPlaylistLoader_.media(nextPlaylist); 46023 } 46024 46025 /** 46026 * Pause all segment loaders 46027 */ 46028 46029 }, { 46030 key: 'pauseLoading', 46031 value: function pauseLoading() { 46032 this.mainSegmentLoader_.pause(); 46033 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46034 this.audioSegmentLoader_.pause(); 46035 } 46036 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 46037 this.subtitleSegmentLoader_.pause(); 46038 } 46039 } 46040 46041 /** 46042 * set the current time on all segment loaders 46043 * 46044 * @param {TimeRange} currentTime the current time to set 46045 * @return {TimeRange} the current time 46046 */ 46047 46048 }, { 46049 key: 'setCurrentTime', 46050 value: function setCurrentTime(currentTime) { 46051 var buffered = findRange(this.tech_.buffered(), currentTime); 46052 46053 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) { 46054 // return immediately if the metadata is not ready yet 46055 return 0; 46056 } 46057 46058 // it's clearly an edge-case but don't thrown an error if asked to 46059 // seek within an empty playlist 46060 if (!this.masterPlaylistLoader_.media().segments) { 46061 return 0; 46062 } 46063 46064 // In flash playback, the segment loaders should be reset on every seek, even 46065 // in buffer seeks. If the seek location is already buffered, continue buffering as 46066 // usual 46067 // TODO: redo this comment 46068 if (buffered && buffered.length) { 46069 return currentTime; 46070 } 46071 46072 // cancel outstanding requests so we begin buffering at the new 46073 // location 46074 this.mainSegmentLoader_.resetEverything(); 46075 this.mainSegmentLoader_.abort(); 46076 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46077 this.audioSegmentLoader_.resetEverything(); 46078 this.audioSegmentLoader_.abort(); 46079 } 46080 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 46081 this.subtitleSegmentLoader_.resetEverything(); 46082 this.subtitleSegmentLoader_.abort(); 46083 } 46084 46085 // start segment loader loading in case they are paused 46086 this.load(); 46087 } 46088 46089 /** 46090 * get the current duration 46091 * 46092 * @return {TimeRange} the duration 46093 */ 46094 46095 }, { 46096 key: 'duration', 46097 value: function duration$$1() { 46098 if (!this.masterPlaylistLoader_) { 46099 return 0; 46100 } 46101 46102 if (this.mediaSource) { 46103 return this.mediaSource.duration; 46104 } 46105 46106 return Hls.Playlist.duration(this.masterPlaylistLoader_.media()); 46107 } 46108 46109 /** 46110 * check the seekable range 46111 * 46112 * @return {TimeRange} the seekable range 46113 */ 46114 46115 }, { 46116 key: 'seekable', 46117 value: function seekable$$1() { 46118 return this.seekable_; 46119 } 46120 }, { 46121 key: 'onSyncInfoUpdate_', 46122 value: function onSyncInfoUpdate_() { 46123 var mainSeekable = void 0; 46124 var audioSeekable = void 0; 46125 46126 if (!this.masterPlaylistLoader_) { 46127 return; 46128 } 46129 46130 var media = this.masterPlaylistLoader_.media(); 46131 46132 if (!media) { 46133 return; 46134 } 46135 46136 var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration); 46137 46138 if (expired === null) { 46139 // not enough information to update seekable 46140 return; 46141 } 46142 46143 mainSeekable = Hls.Playlist.seekable(media, expired); 46144 46145 if (mainSeekable.length === 0) { 46146 return; 46147 } 46148 46149 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46150 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media(); 46151 expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration); 46152 46153 if (expired === null) { 46154 return; 46155 } 46156 46157 audioSeekable = Hls.Playlist.seekable(media, expired); 46158 46159 if (audioSeekable.length === 0) { 46160 return; 46161 } 46162 } 46163 46164 if (!audioSeekable) { 46165 // seekable has been calculated based on buffering video data so it 46166 // can be returned directly 46167 this.seekable_ = mainSeekable; 46168 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) { 46169 // seekables are pretty far off, rely on main 46170 this.seekable_ = mainSeekable; 46171 } else { 46172 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)]]); 46173 } 46174 46175 this.logger_('seekable updated [' + printableRange(this.seekable_) + ']'); 46176 46177 this.tech_.trigger('seekablechanged'); 46178 } 46179 46180 /** 46181 * Update the player duration 46182 */ 46183 46184 }, { 46185 key: 'updateDuration', 46186 value: function updateDuration() { 46187 var _this5 = this; 46188 46189 var oldDuration = this.mediaSource.duration; 46190 var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media()); 46191 var buffered = this.tech_.buffered(); 46192 var setDuration = function setDuration() { 46193 _this5.mediaSource.duration = newDuration; 46194 _this5.tech_.trigger('durationchange'); 46195 46196 _this5.mediaSource.removeEventListener('sourceopen', setDuration); 46197 }; 46198 46199 if (buffered.length > 0) { 46200 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); 46201 } 46202 46203 // if the duration has changed, invalidate the cached value 46204 if (oldDuration !== newDuration) { 46205 // update the duration 46206 if (this.mediaSource.readyState !== 'open') { 46207 this.mediaSource.addEventListener('sourceopen', setDuration); 46208 } else { 46209 setDuration(); 46210 } 46211 } 46212 } 46213 46214 /** 46215 * dispose of the MasterPlaylistController and everything 46216 * that it controls 46217 */ 46218 46219 }, { 46220 key: 'dispose', 46221 value: function dispose() { 46222 var _this6 = this; 46223 46224 this.decrypter_.terminate(); 46225 this.masterPlaylistLoader_.dispose(); 46226 this.mainSegmentLoader_.dispose(); 46227 46228 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 46229 var groups = _this6.mediaTypes_[type].groups; 46230 46231 for (var id in groups) { 46232 groups[id].forEach(function (group) { 46233 if (group.playlistLoader) { 46234 group.playlistLoader.dispose(); 46235 } 46236 }); 46237 } 46238 }); 46239 46240 this.audioSegmentLoader_.dispose(); 46241 this.subtitleSegmentLoader_.dispose(); 46242 } 46243 46244 /** 46245 * return the master playlist object if we have one 46246 * 46247 * @return {Object} the master playlist object that we parsed 46248 */ 46249 46250 }, { 46251 key: 'master', 46252 value: function master() { 46253 return this.masterPlaylistLoader_.master; 46254 } 46255 46256 /** 46257 * return the currently selected playlist 46258 * 46259 * @return {Object} the currently selected playlist object that we parsed 46260 */ 46261 46262 }, { 46263 key: 'media', 46264 value: function media() { 46265 // playlist loader will not return media if it has not been fully loaded 46266 return this.masterPlaylistLoader_.media() || this.initialMedia_; 46267 } 46268 46269 /** 46270 * setup our internal source buffers on our segment Loaders 46271 * 46272 * @private 46273 */ 46274 46275 }, { 46276 key: 'setupSourceBuffers_', 46277 value: function setupSourceBuffers_() { 46278 var media = this.masterPlaylistLoader_.media(); 46279 var mimeTypes = void 0; 46280 46281 // wait until a media playlist is available and the Media Source is 46282 // attached 46283 if (!media || this.mediaSource.readyState !== 'open') { 46284 return; 46285 } 46286 46287 mimeTypes = mimeTypesForPlaylist(this.masterPlaylistLoader_.master, media); 46288 if (mimeTypes.length < 1) { 46289 this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri; 46290 return this.mediaSource.endOfStream('decode'); 46291 } 46292 46293 this.configureLoaderMimeTypes_(mimeTypes); 46294 // exclude any incompatible variant streams from future playlist 46295 // selection 46296 this.excludeIncompatibleVariants_(media); 46297 } 46298 }, { 46299 key: 'configureLoaderMimeTypes_', 46300 value: function configureLoaderMimeTypes_(mimeTypes) { 46301 // If the content is demuxed, we can't start appending segments to a source buffer 46302 // until both source buffers are set up, or else the browser may not let us add the 46303 // second source buffer (it will assume we are playing either audio only or video 46304 // only). 46305 var sourceBufferEmitter = 46306 // If there is more than one mime type 46307 mimeTypes.length > 1 && 46308 // and the first mime type does not have muxed video and audio 46309 mimeTypes[0].indexOf(',') === -1 && 46310 // and the two mime types are different (they can be the same in the case of audio 46311 // only with alternate audio) 46312 mimeTypes[0] !== mimeTypes[1] ? 46313 // then we want to wait on the second source buffer 46314 new videojs$1.EventTarget() : 46315 // otherwise there is no need to wait as the content is either audio only, 46316 // video only, or muxed content. 46317 null; 46318 46319 this.mainSegmentLoader_.mimeType(mimeTypes[0], sourceBufferEmitter); 46320 if (mimeTypes[1]) { 46321 this.audioSegmentLoader_.mimeType(mimeTypes[1], sourceBufferEmitter); 46322 } 46323 } 46324 46325 /** 46326 * Blacklists playlists with codecs that are unsupported by the browser. 46327 */ 46328 46329 }, { 46330 key: 'excludeUnsupportedVariants_', 46331 value: function excludeUnsupportedVariants_() {
46332 this.master().playlists.forEach(function (variant) { 46333 if (variant.attributes.CODECS && window_1.MediaSource && window_1.MediaSource.isTypeSupported && !window_1.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs(variant.attributes.CODECS) + '"')) { 46334 variant.excludeUntil = Infinity; 46335 } 46336 }); 46337 } 46338 46339 /** 46340 * Blacklist playlists that are known to be codec or 46341 * stream-incompatible with the SourceBuffer configuration. For 46342 * instance, Media Source Extensions would cause the video element to 46343 * stall waiting for video data if you switched from a variant with 46344 * video and audio to an audio-only one. 46345 * 46346 * @param {Object} media a media playlist compatible with the current 46347 * set of SourceBuffers. Variants in the current master playlist that 46348 * do not appear to have compatible codec or stream configurations 46349 * will be excluded from the default playlist selection algorithm 46350 * indefinitely. 46351 * @private 46352 */ 46353 46354 }, { 46355 key: 'excludeIncompatibleVariants_', 46356 value: function excludeIncompatibleVariants_(media) { 46357 var codecCount = 2; 46358 var videoCodec = null; 46359 var codecs = void 0; 46360 46361 if (media.attributes.CODECS) { 46362 codecs = parseCodecs(media.attributes.CODECS); 46363 videoCodec = codecs.videoCodec; 46364 codecCount = codecs.codecCount; 46365 } 46366
vendor: 49,600 bytes, lines 46367-47810
46367 this.master().playlists.forEach(function (variant) { 46368 var variantCodecs = { 46369 codecCount: 2, 46370 videoCodec: null 46371 }; 46372 46373 if (variant.attributes.CODECS) { 46374 variantCodecs = parseCodecs(variant.attributes.CODECS); 46375 } 46376 46377 // if the streams differ in the presence or absence of audio or 46378 // video, they are incompatible 46379 if (variantCodecs.codecCount !== codecCount) { 46380 variant.excludeUntil = Infinity; 46381 } 46382 46383 // if h.264 is specified on the current playlist, some flavor of 46384 // it must be specified on all compatible variants 46385 if (variantCodecs.videoCodec !== videoCodec) { 46386 variant.excludeUntil = Infinity; 46387 } 46388 }); 46389 } 46390 }, { 46391 key: 'updateAdCues_', 46392 value: function updateAdCues_(media) { 46393 var offset = 0; 46394 var seekable$$1 = this.seekable(); 46395 46396 if (seekable$$1.length) { 46397 offset = seekable$$1.start(0); 46398 } 46399 46400 updateAdCues(media, this.cueTagsTrack_, offset); 46401 } 46402 46403 /** 46404 * Calculates the desired forward buffer length based on current time 46405 * 46406 * @return {Number} Desired forward buffer length in seconds 46407 */ 46408 46409 }, { 46410 key: 'goalBufferLength', 46411 value: function goalBufferLength() { 46412 var currentTime = this.tech_.currentTime(); 46413 var initial = Config.GOAL_BUFFER_LENGTH; 46414 var rate = Config.GOAL_BUFFER_LENGTH_RATE; 46415 var max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 46416 46417 return Math.min(initial + currentTime * rate, max); 46418 } 46419 46420 /** 46421 * Calculates the desired buffer low water line based on current time 46422 * 46423 * @return {Number} Desired buffer low water line in seconds 46424 */ 46425 46426 }, { 46427 key: 'bufferLowWaterLine', 46428 value: function bufferLowWaterLine() { 46429 var currentTime = this.tech_.currentTime(); 46430 var initial = Config.BUFFER_LOW_WATER_LINE; 46431 var rate = Config.BUFFER_LOW_WATER_LINE_RATE; 46432 var max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 46433 46434 return Math.min(initial + currentTime * rate, max); 46435 } 46436 }]); 46437 return MasterPlaylistController; 46438 }(videojs$1.EventTarget); 46439 46440 /** 46441 * Returns a function that acts as the Enable/disable playlist function. 46442 * 46443 * @param {PlaylistLoader} loader - The master playlist loader 46444 * @param {String} playlistUri - uri of the playlist 46445 * @param {Function} changePlaylistFn - A function to be called after a 46446 * playlist's enabled-state has been changed. Will NOT be called if a 46447 * playlist's enabled-state is unchanged 46448 * @param {Boolean=} enable - Value to set the playlist enabled-state to 46449 * or if undefined returns the current enabled-state for the playlist 46450 * @return {Function} Function for setting/getting enabled 46451 */ 46452 var enableFunction = function enableFunction(loader, playlistUri, changePlaylistFn) { 46453 return function (enable) { 46454 var playlist = loader.master.playlists[playlistUri]; 46455 var incompatible = isIncompatible(playlist); 46456 var currentlyEnabled = isEnabled(playlist); 46457 46458 if (typeof enable === 'undefined') { 46459 return currentlyEnabled; 46460 } 46461 46462 if (enable) { 46463 delete playlist.disabled; 46464 } else { 46465 playlist.disabled = true; 46466 } 46467 46468 if (enable !== currentlyEnabled && !incompatible) { 46469 // Ensure the outside world knows about our changes 46470 changePlaylistFn(); 46471 if (enable) { 46472 loader.trigger('renditionenabled'); 46473 } else { 46474 loader.trigger('renditiondisabled'); 46475 } 46476 } 46477 return enable; 46478 }; 46479 }; 46480 46481 /** 46482 * The representation object encapsulates the publicly visible information 46483 * in a media playlist along with a setter/getter-type function (enabled) 46484 * for changing the enabled-state of a particular playlist entry 46485 * 46486 * @class Representation 46487 */ 46488 46489 var Representation = function Representation(hlsHandler, playlist, id) { 46490 classCallCheck$3(this, Representation); 46491 46492 // Get a reference to a bound version of fastQualityChange_ 46493 var fastChangeFunction = hlsHandler.masterPlaylistController_.fastQualityChange_.bind(hlsHandler.masterPlaylistController_); 46494 46495 // some playlist attributes are optional 46496 if (playlist.attributes.RESOLUTION) { 46497 var resolution = playlist.attributes.RESOLUTION; 46498 46499 this.width = resolution.width; 46500 this.height = resolution.height; 46501 } 46502 46503 this.bandwidth = playlist.attributes.BANDWIDTH; 46504 46505 // The id is simply the ordinality of the media playlist 46506 // within the master playlist 46507 this.id = id; 46508 46509 // Partially-apply the enableFunction to create a playlist- 46510 // specific variant 46511 this.enabled = enableFunction(hlsHandler.playlists, playlist.uri, fastChangeFunction); 46512 }; 46513 46514 /** 46515 * A mixin function that adds the `representations` api to an instance 46516 * of the HlsHandler class 46517 * @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the 46518 * representation API into 46519 */ 46520 46521 var renditionSelectionMixin = function renditionSelectionMixin(hlsHandler) { 46522 var playlists = hlsHandler.playlists; 46523 46524 // Add a single API-specific function to the HlsHandler instance 46525 hlsHandler.representations = function () { 46526 return playlists.master.playlists.filter(function (media) { 46527 return !isIncompatible(media); 46528 }).map(function (e, i) { 46529 return new Representation(hlsHandler, e, e.uri); 46530 }); 46531 }; 46532 }; 46533 46534 /** 46535 * @file playback-watcher.js 46536 * 46537 * Playback starts, and now my watch begins. It shall not end until my death. I shall 46538 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 46539 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 46540 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 46541 * my life and honor to the Playback Watch, for this Player and all the Players to come. 46542 */ 46543 46544 // Set of events that reset the playback-watcher time check logic and clear the timeout 46545 var timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 46546 46547 /** 46548 * @class PlaybackWatcher 46549 */ 46550 46551 var PlaybackWatcher = function () { 46552 /** 46553 * Represents an PlaybackWatcher object. 46554 * @constructor 46555 * @param {object} options an object that includes the tech and settings 46556 */ 46557 function PlaybackWatcher(options) { 46558 var _this = this; 46559 46560 classCallCheck$3(this, PlaybackWatcher); 46561 46562 this.tech_ = options.tech; 46563 this.seekable = options.seekable; 46564 46565 this.consecutiveUpdates = 0; 46566 this.lastRecordedTime = null; 46567 this.timer_ = null; 46568 this.checkCurrentTimeTimeout_ = null; 46569 this.logger_ = logger('PlaybackWatcher'); 46570 46571 this.logger_('initialize'); 46572 46573 var canPlayHandler = function canPlayHandler() { 46574 return _this.monitorCurrentTime_(); 46575 }; 46576 var waitingHandler = function waitingHandler() { 46577 return _this.techWaiting_(); 46578 }; 46579 var cancelTimerHandler = function cancelTimerHandler() { 46580 return _this.cancelTimer_(); 46581 }; 46582 var fixesBadSeeksHandler = function fixesBadSeeksHandler() { 46583 return _this.fixesBadSeeks_(); 46584 }; 46585 46586 this.tech_.on('seekablechanged', fixesBadSeeksHandler); 46587 this.tech_.on('waiting', waitingHandler); 46588 this.tech_.on(timerCancelEvents, cancelTimerHandler); 46589 this.tech_.on('canplay', canPlayHandler); 46590 46591 // Define the dispose function to clean up our events 46592 this.dispose = function () { 46593 _this.logger_('dispose'); 46594 _this.tech_.off('seekablechanged', fixesBadSeeksHandler); 46595 _this.tech_.off('waiting', waitingHandler); 46596 _this.tech_.off(timerCancelEvents, cancelTimerHandler); 46597 _this.tech_.off('canplay', canPlayHandler); 46598 if (_this.checkCurrentTimeTimeout_) { 46599 window_1.clearTimeout(_this.checkCurrentTimeTimeout_); 46600 } 46601 _this.cancelTimer_(); 46602 }; 46603 } 46604 46605 /** 46606 * Periodically check current time to see if playback stopped 46607 * 46608 * @private 46609 */ 46610 46611 createClass$2(PlaybackWatcher, [{ 46612 key: 'monitorCurrentTime_', 46613 value: function monitorCurrentTime_() { 46614 this.checkCurrentTime_(); 46615 46616 if (this.checkCurrentTimeTimeout_) { 46617 window_1.clearTimeout(this.checkCurrentTimeTimeout_); 46618 } 46619 46620 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 46621 this.checkCurrentTimeTimeout_ = window_1.setTimeout(this.monitorCurrentTime_.bind(this), 250); 46622 } 46623 46624 /** 46625 * The purpose of this function is to emulate the "waiting" event on 46626 * browsers that do not emit it when they are waiting for more 46627 * data to continue playback 46628 * 46629 * @private 46630 */ 46631 46632 }, { 46633 key: 'checkCurrentTime_', 46634 value: function checkCurrentTime_() { 46635 if (this.tech_.seeking() && this.fixesBadSeeks_()) { 46636 this.consecutiveUpdates = 0; 46637 this.lastRecordedTime = this.tech_.currentTime(); 46638 return; 46639 } 46640 46641 if (this.tech_.paused() || this.tech_.seeking()) { 46642 return; 46643 } 46644 46645 var currentTime = this.tech_.currentTime(); 46646 var buffered = this.tech_.buffered(); 46647 46648 if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 46649 // If current time is at the end of the final buffered region, then any playback 46650 // stall is most likely caused by buffering in a low bandwidth environment. The tech 46651 // should fire a `waiting` event in this scenario, but due to browser and tech 46652 // inconsistencies. Calling `techWaiting_` here allows us to simulate 46653 // responding to a native `waiting` event when the tech fails to emit one. 46654 return this.techWaiting_(); 46655 } 46656 46657 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 46658 this.consecutiveUpdates++; 46659 this.waiting_(); 46660 } else if (currentTime === this.lastRecordedTime) { 46661 this.consecutiveUpdates++; 46662 } else { 46663 this.consecutiveUpdates = 0; 46664 this.lastRecordedTime = currentTime; 46665 } 46666 } 46667 46668 /** 46669 * Cancels any pending timers and resets the 'timeupdate' mechanism 46670 * designed to detect that we are stalled 46671 * 46672 * @private 46673 */ 46674 46675 }, { 46676 key: 'cancelTimer_', 46677 value: function cancelTimer_() { 46678 this.consecutiveUpdates = 0; 46679 46680 if (this.timer_) { 46681 this.logger_('cancelTimer_'); 46682 clearTimeout(this.timer_); 46683 } 46684 46685 this.timer_ = null; 46686 } 46687 46688 /** 46689 * Fixes situations where there's a bad seek 46690 * 46691 * @return {Boolean} whether an action was taken to fix the seek 46692 * @private 46693 */ 46694 46695 }, { 46696 key: 'fixesBadSeeks_', 46697 value: function fixesBadSeeks_() { 46698 var seeking = this.tech_.seeking(); 46699 var seekable = this.seekable(); 46700 var currentTime = this.tech_.currentTime(); 46701 var seekTo = void 0; 46702 46703 if (seeking && this.afterSeekableWindow_(seekable, currentTime)) { 46704 var seekableEnd = seekable.end(seekable.length - 1); 46705 46706 // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 46707 seekTo = seekableEnd; 46708 } 46709 46710 if (seeking && this.beforeSeekableWindow_(seekable, currentTime)) { 46711 var seekableStart = seekable.start(0); 46712 46713 // sync to the beginning of the live window 46714 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 46715 seekTo = seekableStart + SAFE_TIME_DELTA; 46716 } 46717 46718 if (typeof seekTo !== 'undefined') { 46719 this.logger_('Trying to seek outside of seekable at time ' + currentTime + ' with ' + ('seekable range ' + printableRange(seekable) + '. Seeking to ') + (seekTo + '.')); 46720 46721 this.tech_.setCurrentTime(seekTo); 46722 return true; 46723 } 46724 46725 return false; 46726 } 46727 46728 /** 46729 * Handler for situations when we determine the player is waiting. 46730 * 46731 * @private 46732 */ 46733 46734 }, { 46735 key: 'waiting_', 46736 value: function waiting_() { 46737 if (this.techWaiting_()) { 46738 return; 46739 } 46740 46741 // All tech waiting checks failed. Use last resort correction 46742 var currentTime = this.tech_.currentTime(); 46743 var buffered = this.tech_.buffered(); 46744 var currentRange = findRange(buffered, currentTime); 46745 46746 // Sometimes the player can stall for unknown reasons within a contiguous buffered 46747 // region with no indication that anything is amiss (seen in Firefox). Seeking to 46748 // currentTime is usually enough to kickstart the player. This checks that the player 46749 // is currently within a buffered region before attempting a corrective seek. 46750 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 46751 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 46752 // make sure there is ~3 seconds of forward buffer before taking any corrective action 46753 // to avoid triggering an `unknownwaiting` event when the network is slow. 46754 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 46755 this.cancelTimer_(); 46756 this.tech_.setCurrentTime(currentTime); 46757 46758 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.'); 46759 46760 // unknown waiting corrections may be useful for monitoring QoS 46761 this.tech_.trigger({ type: 'usage', name: 'hls-unknown-waiting' }); 46762 return; 46763 } 46764 } 46765 46766 /** 46767 * Handler for situations when the tech fires a `waiting` event 46768 * 46769 * @return {Boolean} 46770 * True if an action (or none) was needed to correct the waiting. False if no 46771 * checks passed 46772 * @private 46773 */ 46774 46775 }, { 46776 key: 'techWaiting_', 46777 value: function techWaiting_() { 46778 var seekable = this.seekable(); 46779 var currentTime = this.tech_.currentTime(); 46780 46781 if (this.tech_.seeking() && this.fixesBadSeeks_()) { 46782 // Tech is seeking or bad seek fixed, no action needed 46783 return true; 46784 } 46785 46786 if (this.tech_.seeking() || this.timer_ !== null) { 46787 // Tech is seeking or already waiting on another action, no action needed 46788 return true; 46789 } 46790 46791 if (this.beforeSeekableWindow_(seekable, currentTime)) { 46792 var livePoint = seekable.end(seekable.length - 1); 46793 46794 this.logger_('Fell out of live window at time ' + currentTime + '. Seeking to ' + ('live point (seekable end) ' + livePoint)); 46795 this.cancelTimer_(); 46796 this.tech_.setCurrentTime(livePoint); 46797 46798 // live window resyncs may be useful for monitoring QoS 46799 this.tech_.trigger({ type: 'usage', name: 'hls-live-resync' }); 46800 return true; 46801 } 46802 46803 var buffered = this.tech_.buffered(); 46804 var nextRange = findNextRange(buffered, currentTime); 46805 46806 if (this.videoUnderflow_(nextRange, buffered, currentTime)) { 46807 // Even though the video underflowed and was stuck in a gap, the audio overplayed 46808 // the gap, leading currentTime into a buffered range. Seeking to currentTime 46809 // allows the video to catch up to the audio position without losing any audio 46810 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 46811 this.cancelTimer_(); 46812 this.tech_.setCurrentTime(currentTime); 46813 46814 // video underflow may be useful for monitoring QoS 46815 this.tech_.trigger({ type: 'usage', name: 'hls-video-underflow' }); 46816 return true; 46817 } 46818 46819 // check for gap 46820 if (nextRange.length > 0) { 46821 var difference = nextRange.start(0) - currentTime; 46822 46823 this.logger_('Stopped at ' + currentTime + ', setting timer for ' + difference + ', seeking ' + ('to ' + nextRange.start(0))); 46824 46825 this.timer_ = setTimeout(this.skipTheGap_.bind(this), difference * 1000, currentTime); 46826 return true; 46827 } 46828 46829 // All checks failed. Returning false to indicate failure to correct waiting 46830 return false; 46831 } 46832 }, { 46833 key: 'afterSeekableWindow_', 46834 value: function afterSeekableWindow_(seekable, currentTime) { 46835 if (!seekable.length) { 46836 // we can't make a solid case if there's no seekable, default to false 46837 return false; 46838 } 46839 46840 if (currentTime > seekable.end(seekable.length - 1) + SAFE_TIME_DELTA) { 46841 return true; 46842 } 46843 46844 return false; 46845 } 46846 }, { 46847 key: 'beforeSeekableWindow_', 46848 value: function beforeSeekableWindow_(seekable, currentTime) { 46849 if (seekable.length && 46850 // can't fall before 0 and 0 seekable start identifies VOD stream 46851 seekable.start(0) > 0 && currentTime < seekable.start(0) - SAFE_TIME_DELTA) { 46852 return true; 46853 } 46854 46855 return false; 46856 } 46857 }, { 46858 key: 'videoUnderflow_', 46859 value: function videoUnderflow_(nextRange, buffered, currentTime) { 46860 if (nextRange.length === 0) { 46861 // Even if there is no available next range, there is still a possibility we are 46862 // stuck in a gap due to video underflow. 46863 var gap = this.gapFromVideoUnderflow_(buffered, currentTime); 46864 46865 if (gap) { 46866 this.logger_('Encountered a gap in video from ' + gap.start + ' to ' + gap.end + '. ' + ('Seeking to current time ' + currentTime)); 46867 46868 return true; 46869 } 46870 } 46871 46872 return false; 46873 } 46874 46875 /** 46876 * Timer callback. If playback still has not proceeded, then we seek 46877 * to the start of the next buffered region. 46878 * 46879 * @private 46880 */ 46881 46882 }, { 46883 key: 'skipTheGap_', 46884 value: function skipTheGap_(scheduledCurrentTime) { 46885 var buffered = this.tech_.buffered(); 46886 var currentTime = this.tech_.currentTime(); 46887 var nextRange = findNextRange(buffered, currentTime); 46888 46889 this.cancelTimer_(); 46890 46891 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 46892 return; 46893 } 46894 46895 this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); 46896 46897 // only seek if we still have not played 46898 this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR); 46899 46900 this.tech_.trigger({ type: 'usage', name: 'hls-gap-skip' }); 46901 } 46902 }, { 46903 key: 'gapFromVideoUnderflow_', 46904 value: function gapFromVideoUnderflow_(buffered, currentTime) { 46905 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 46906 // playing for ~3 seconds after the video gap starts. This is done to account for 46907 // video buffer underflow/underrun (note that this is not done when there is audio 46908 // buffer underflow/underrun -- in that case the video will stop as soon as it 46909 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 46910 // video stalls). The player's time will reflect the playthrough of audio, so the 46911 // time will appear as if we are in a buffered region, even if we are stuck in a 46912 // "gap." 46913 // 46914 // Example: 46915 // video buffer: 0 => 10.1, 10.2 => 20 46916 // audio buffer: 0 => 20 46917 // overall buffer: 0 => 10.1, 10.2 => 20 46918 // current time: 13 46919 // 46920 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 46921 // however, the audio continued playing until it reached ~3 seconds past the gap 46922 // (13 seconds), at which point it stops as well. Since current time is past the 46923 // gap, findNextRange will return no ranges. 46924 // 46925 // To check for this issue, we see if there is a gap that starts somewhere within 46926 // a 3 second range (3 seconds +/- 1 second) back from our current time. 46927 var gaps = findGaps(buffered); 46928 46929 for (var i = 0; i < gaps.length; i++) { 46930 var start = gaps.start(i); 46931 var end = gaps.end(i); 46932 46933 // gap is starts no more than 4 seconds back 46934 if (currentTime - start < 4 && currentTime - start > 2) { 46935 return { 46936 start: start, 46937 end: end 46938 }; 46939 } 46940 } 46941 46942 return null; 46943 } 46944 }]); 46945 return PlaybackWatcher; 46946 }(); 46947 46948 var defaultOptions = { 46949 errorInterval: 30, 46950 getSource: function getSource(next) { 46951 var tech = this.tech({ IWillNotUseThisInPlugins: true }); 46952 var sourceObj = tech.currentSource_; 46953 46954 return next(sourceObj); 46955 } 46956 }; 46957 46958 /** 46959 * Main entry point for the plugin 46960 * 46961 * @param {Player} player a reference to a videojs Player instance 46962 * @param {Object} [options] an object with plugin options 46963 * @private 46964 */ 46965 var initPlugin = function initPlugin(player, options) { 46966 var lastCalled = 0; 46967 var seekTo = 0; 46968 var localOptions = videojs$1.mergeOptions(defaultOptions, options); 46969 46970 player.ready(function () { 46971 player.trigger({ type: 'usage', name: 'hls-error-reload-initialized' }); 46972 }); 46973 46974 /** 46975 * Player modifications to perform that must wait until `loadedmetadata` 46976 * has been triggered 46977 * 46978 * @private 46979 */ 46980 var loadedMetadataHandler = function loadedMetadataHandler() { 46981 if (seekTo) { 46982 player.currentTime(seekTo); 46983 } 46984 }; 46985 46986 /** 46987 * Set the source on the player element, play, and seek if necessary 46988 * 46989 * @param {Object} sourceObj An object specifying the source url and mime-type to play 46990 * @private 46991 */ 46992 var setSource = function setSource(sourceObj) { 46993 if (sourceObj === null || sourceObj === undefined) { 46994 return; 46995 } 46996 seekTo = player.duration() !== Infinity && player.currentTime() || 0; 46997 46998 player.one('loadedmetadata', loadedMetadataHandler); 46999 47000 player.src(sourceObj); 47001 player.trigger({ type: 'usage', name: 'hls-error-reload' }); 47002 player.play(); 47003 }; 47004 47005 /** 47006 * Attempt to get a source from either the built-in getSource function 47007 * or a custom function provided via the options 47008 * 47009 * @private 47010 */ 47011 var errorHandler = function errorHandler() { 47012 // Do not attempt to reload the source if a source-reload occurred before 47013 // 'errorInterval' time has elapsed since the last source-reload 47014 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 47015 player.trigger({ type: 'usage', name: 'hls-error-reload-canceled' }); 47016 return; 47017 } 47018 47019 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 47020 videojs$1.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 47021 return; 47022 } 47023 lastCalled = Date.now(); 47024 47025 return localOptions.getSource.call(player, setSource); 47026 }; 47027 47028 /** 47029 * Unbind any event handlers that were bound by the plugin 47030 * 47031 * @private 47032 */ 47033 var cleanupEvents = function cleanupEvents() { 47034 player.off('loadedmetadata', loadedMetadataHandler); 47035 player.off('error', errorHandler); 47036 player.off('dispose', cleanupEvents); 47037 }; 47038 47039 /** 47040 * Cleanup before re-initializing the plugin 47041 * 47042 * @param {Object} [newOptions] an object with plugin options 47043 * @private 47044 */ 47045 var reinitPlugin = function reinitPlugin(newOptions) { 47046 cleanupEvents(); 47047 initPlugin(player, newOptions); 47048 }; 47049 47050 player.on('error', errorHandler); 47051 player.on('dispose', cleanupEvents); 47052 47053 // Overwrite the plugin function so that we can correctly cleanup before 47054 // initializing the plugin 47055 player.reloadSourceOnError = reinitPlugin; 47056 }; 47057 47058 /** 47059 * Reload the source when an error is detected as long as there 47060 * wasn't an error previously within the last 30 seconds 47061 * 47062 * @param {Object} [options] an object with plugin options 47063 */ 47064 var reloadSourceOnError = function reloadSourceOnError(options) { 47065 initPlugin(this, options); 47066 }; 47067 47068 var version$3 = "1.0.2"; 47069 47070 /** 47071 * @file videojs-http-streaming.js 47072 * 47073 * The main file for the HLS project. 47074 * License: https://github.com/videojs/videojs-http-streaming/blob/master/LICENSE 47075 */ 47076 47077 var Hls$1 = { 47078 PlaylistLoader: PlaylistLoader, 47079 Playlist: Playlist, 47080 Decrypter: Decrypter, 47081 AsyncStream: AsyncStream, 47082 decrypt: decrypt, 47083 utils: utils, 47084 47085 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 47086 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 47087 comparePlaylistBandwidth: comparePlaylistBandwidth, 47088 comparePlaylistResolution: comparePlaylistResolution, 47089 47090 xhr: xhrFactory() 47091 }; 47092 47093 // 0.5 MB/s 47094 var INITIAL_BANDWIDTH = 4194304; 47095 47096 // Define getter/setters for config properites 47097 ['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) { 47098 Object.defineProperty(Hls$1, prop, { 47099 get: function get$$1() { 47100 videojs$1.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); 47101 return Config[prop]; 47102 }, 47103 set: function set$$1(value) { 47104 videojs$1.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); 47105 47106 if (typeof value !== 'number' || value < 0) { 47107 videojs$1.log.warn('value of Hls.' + prop + ' must be greater than or equal to 0'); 47108 return; 47109 } 47110 47111 Config[prop] = value; 47112 } 47113 }); 47114 }); 47115 47116 var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) { 47117 var mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; 47118 47119 if (mpegurlRE.test(type)) { 47120 return 'hls'; 47121 } 47122 47123 var dashRE = /^application\/dash\+xml/i; 47124 47125 if (dashRE.test(type)) { 47126 return 'dash'; 47127 } 47128 47129 return null; 47130 }; 47131 47132 /** 47133 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls. 47134 * 47135 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 47136 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 47137 * @function handleHlsMediaChange 47138 */ 47139 var handleHlsMediaChange = function handleHlsMediaChange(qualityLevels, playlistLoader) { 47140 var newPlaylist = playlistLoader.media(); 47141 var selectedIndex = -1; 47142 47143 for (var i = 0; i < qualityLevels.length; i++) { 47144 if (qualityLevels[i].id === newPlaylist.uri) { 47145 selectedIndex = i; 47146 break; 47147 } 47148 } 47149 47150 qualityLevels.selectedIndex_ = selectedIndex; 47151 qualityLevels.trigger({ 47152 selectedIndex: selectedIndex, 47153 type: 'change' 47154 }); 47155 }; 47156 47157 /** 47158 * Adds quality levels to list once playlist metadata is available 47159 * 47160 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 47161 * @param {Object} hls Hls object to listen to for media events. 47162 * @function handleHlsLoadedMetadata 47163 */ 47164 var handleHlsLoadedMetadata = function handleHlsLoadedMetadata(qualityLevels, hls) { 47165 hls.representations().forEach(function (rep) { 47166 qualityLevels.addQualityLevel(rep); 47167 }); 47168 handleHlsMediaChange(qualityLevels, hls.playlists); 47169 }; 47170 47171 // HLS is a source handler, not a tech. Make sure attempts to use it 47172 // as one do not cause exceptions. 47173 Hls$1.canPlaySource = function () { 47174 return videojs$1.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 47175 }; 47176 47177 var emeKeySystems = function emeKeySystems(keySystemOptions, videoPlaylist, audioPlaylist) { 47178 if (!keySystemOptions) { 47179 return keySystemOptions; 47180 } 47181 47182 // upsert the content types based on the selected playlist 47183 var keySystemContentTypes = {}; 47184 47185 for (var keySystem in keySystemOptions) { 47186 keySystemContentTypes[keySystem] = { 47187 audioContentType: 'audio/mp4; codecs="' + audioPlaylist.attributes.CODECS + '"', 47188 videoContentType: 'video/mp4; codecs="' + videoPlaylist.attributes.CODECS + '"' 47189 }; 47190 47191 if (videoPlaylist.contentProtection && videoPlaylist.contentProtection[keySystem] && videoPlaylist.contentProtection[keySystem].pssh) { 47192 keySystemContentTypes[keySystem].pssh = videoPlaylist.contentProtection[keySystem].pssh; 47193 } 47194 47195 // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url' 47196 // so we need to prevent overwriting the URL entirely 47197 if (typeof keySystemOptions[keySystem] === 'string') { 47198 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 47199 } 47200 } 47201 47202 return videojs$1.mergeOptions(keySystemOptions, keySystemContentTypes); 47203 }; 47204 47205 var setupEmeOptions = function setupEmeOptions(hlsHandler) { 47206 if (hlsHandler.options_.sourceType !== 'dash') { 47207 return; 47208 } 47209 var player = videojs$1.players[hlsHandler.tech_.options_.playerId]; 47210 47211 if (player.eme) { 47212 var sourceOptions = emeKeySystems(hlsHandler.source_.keySystems, hlsHandler.playlists.media(), hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader.media()); 47213 47214 if (sourceOptions) { 47215 player.currentSource().keySystems = sourceOptions; 47216 } 47217 } 47218 }; 47219 47220 /** 47221 * Whether the browser has built-in HLS support. 47222 */ 47223 Hls$1.supportsNativeHls = function () { 47224 var video = document_1.createElement('video'); 47225 47226 // native HLS is definitely not supported if HTML5 video isn't 47227 if (!videojs$1.getTech('Html5').isSupported()) { 47228 return false; 47229 } 47230 47231 // HLS manifests can go by many mime-types 47232 var canPlay = [ 47233 // Apple santioned 47234 'application/vnd.apple.mpegurl', 47235 // Apple sanctioned for backwards compatibility 47236 'audio/mpegurl', 47237 // Very common 47238 'audio/x-mpegurl', 47239 // Very common 47240 'application/x-mpegurl', 47241 // Included for completeness 47242 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; 47243 47244 return canPlay.some(function (canItPlay) { 47245 return (/maybe|probably/i.test(video.canPlayType(canItPlay)) 47246 ); 47247 }); 47248 }(); 47249 47250 Hls$1.supportsNativeDash = function () { 47251 if (!videojs$1.getTech('Html5').isSupported()) { 47252 return false; 47253 } 47254 47255 return (/maybe|probably/i.test(document_1.createElement('video').canPlayType('application/dash+xml')) 47256 ); 47257 }(); 47258 47259 Hls$1.supportsTypeNatively = function (type) { 47260 if (type === 'hls') { 47261 return Hls$1.supportsNativeHls; 47262 } 47263 47264 if (type === 'dash') { 47265 return Hls$1.supportsNativeDash; 47266 } 47267 47268 return false; 47269 }; 47270 47271 /** 47272 * HLS is a source handler, not a tech. Make sure attempts to use it 47273 * as one do not cause exceptions. 47274 */ 47275 Hls$1.isSupported = function () { 47276 return videojs$1.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 47277 }; 47278 47279 var Component$1 = videojs$1.getComponent('Component'); 47280 47281 /** 47282 * The Hls Handler object, where we orchestrate all of the parts 47283 * of HLS to interact with video.js 47284 * 47285 * @class HlsHandler 47286 * @extends videojs.Component 47287 * @param {Object} source the soruce object 47288 * @param {Tech} tech the parent tech object 47289 * @param {Object} options optional and required options 47290 */ 47291 47292 var HlsHandler = function (_Component) { 47293 inherits$3(HlsHandler, _Component); 47294 47295 function HlsHandler(source, tech, options) { 47296 classCallCheck$3(this, HlsHandler); 47297 47298 // tech.player() is deprecated but setup a reference to HLS for 47299 // backwards-compatibility 47300 var _this = possibleConstructorReturn$3(this, (HlsHandler.__proto__ || Object.getPrototypeOf(HlsHandler)).call(this, tech, options.hls)); 47301 47302 if (tech.options_ && tech.options_.playerId) { 47303 var _player = videojs$1(tech.options_.playerId); 47304 47305 if (!_player.hasOwnProperty('hls')) { 47306 Object.defineProperty(_player, 'hls', { 47307 get: function get$$1() { 47308 videojs$1.log.warn('player.hls is deprecated. Use player.tech_.hls instead.'); 47309 tech.trigger({ type: 'usage', name: 'hls-player-access' }); 47310 return _this; 47311 } 47312 }); 47313 } 47314 47315 // Set up a reference to the HlsHandler from player.vhs. This allows users to start 47316 // migrating from player.tech_.hls... to player.vhs... for API access. Although this 47317 // isn't the most appropriate form of reference for video.js (since all APIs should 47318 // be provided through core video.js), it is a common pattern for plugins, and vhs 47319 // will act accordingly. 47320 _player.vhs = _this; 47321 // deprecated, for backwards compatibility 47322 _player.dash = _this; 47323 } 47324 47325 _this.tech_ = tech; 47326 _this.source_ = source; 47327 _this.stats = {}; 47328 _this.ignoreNextSeekingEvent_ = false; 47329 _this.setOptions_(); 47330 47331 // overriding native HLS only works if audio tracks have been emulated 47332 // error early if we're misconfigured: 47333 if (_this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { 47334 throw new Error('Overriding native HLS requires emulated tracks. ' + 'See https://git.io/vMpjB'); 47335 } 47336 47337 // listen for fullscreenchange events for this player so that we 47338 // can adjust our quality selection quickly 47339 _this.on(document_1, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], function (event) { 47340 var fullscreenElement = document_1.fullscreenElement || document_1.webkitFullscreenElement || document_1.mozFullScreenElement || document_1.msFullscreenElement; 47341 47342 if (fullscreenElement && fullscreenElement.contains(_this.tech_.el())) { 47343 _this.masterPlaylistController_.fastQualityChange_(); 47344 } 47345 }); 47346 47347 _this.on(_this.tech_, 'seeking', function () { 47348 if (this.ignoreNextSeekingEvent_) { 47349 this.ignoreNextSeekingEvent_ = false; 47350 return; 47351 } 47352 47353 this.setCurrentTime(this.tech_.currentTime()); 47354 }); 47355 _this.on(_this.tech_, 'error', function () { 47356 if (this.masterPlaylistController_) { 47357 this.masterPlaylistController_.pauseLoading(); 47358 } 47359 }); 47360 47361 _this.on(_this.tech_, 'play', _this.play); 47362 return _this; 47363 } 47364 47365 createClass$2(HlsHandler, [{ 47366 key: 'setOptions_', 47367 value: function setOptions_() { 47368 var _this2 = this; 47369 47370 // defaults 47371 this.options_.withCredentials = this.options_.withCredentials || false; 47372 47373 if (typeof this.options_.blacklistDuration !== 'number') { 47374 this.options_.blacklistDuration = 5 * 60; 47375 } 47376 47377 // start playlist selection at a reasonable bandwidth for 47378 // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s) 47379 if (typeof this.options_.bandwidth !== 'number') { 47380 this.options_.bandwidth = INITIAL_BANDWIDTH; 47381 } 47382 47383 // If the bandwidth number is unchanged from the initial setting 47384 // then this takes precedence over the enableLowInitialPlaylist option 47385 this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === INITIAL_BANDWIDTH; 47386 47387 // grab options passed to player.src 47388 ['withCredentials', 'bandwidth'].forEach(function (option) { 47389 if (typeof _this2.source_[option] !== 'undefined') { 47390 _this2.options_[option] = _this2.source_[option]; 47391 } 47392 }); 47393 47394 this.bandwidth = this.options_.bandwidth; 47395 } 47396 /** 47397 * called when player.src gets called, handle a new source 47398 * 47399 * @param {Object} src the source object to handle 47400 */ 47401 47402 }, { 47403 key: 'src', 47404 value: function src(_src, type) { 47405 var _this3 = this; 47406 47407 // do nothing if the src is falsey 47408 if (!_src) { 47409 return; 47410 } 47411 this.setOptions_(); 47412 // add master playlist controller options 47413 this.options_.url = this.source_.src; 47414 this.options_.tech = this.tech_; 47415 this.options_.externHls = Hls$1; 47416 this.options_.sourceType = simpleTypeFromSourceType(type); 47417 47418 this.masterPlaylistController_ = new MasterPlaylistController(this.options_); 47419 this.playbackWatcher_ = new PlaybackWatcher(videojs$1.mergeOptions(this.options_, { 47420 seekable: function seekable$$1() { 47421 return _this3.seekable(); 47422 } 47423 })); 47424 47425 this.masterPlaylistController_.on('error', function () { 47426 var player = videojs$1.players[_this3.tech_.options_.playerId]; 47427 47428 player.error(_this3.masterPlaylistController_.error); 47429 }); 47430 47431 // `this` in selectPlaylist should be the HlsHandler for backwards 47432 // compatibility with < v2 47433 this.masterPlaylistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls$1.STANDARD_PLAYLIST_SELECTOR.bind(this); 47434 47435 this.masterPlaylistController_.selectInitialPlaylist = Hls$1.INITIAL_PLAYLIST_SELECTOR.bind(this); 47436 47437 // re-expose some internal objects for backwards compatibility with < v2 47438 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; 47439 this.mediaSource = this.masterPlaylistController_.mediaSource; 47440 47441 // Proxy assignment of some properties to the master playlist 47442 // controller. Using a custom property for backwards compatibility 47443 // with < v2 47444 Object.defineProperties(this, { 47445 selectPlaylist: { 47446 get: function get$$1() { 47447 return this.masterPlaylistController_.selectPlaylist; 47448 }, 47449 set: function set$$1(selectPlaylist) { 47450 this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this); 47451 } 47452 }, 47453 throughput: { 47454 get: function get$$1() { 47455 return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate; 47456 }, 47457 set: function set$$1(throughput) { 47458 this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput; 47459 // By setting `count` to 1 the throughput value becomes the starting value 47460 // for the cumulative average 47461 this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1; 47462 } 47463 }, 47464 bandwidth: { 47465 get: function get$$1() { 47466 return this.masterPlaylistController_.mainSegmentLoader_.bandwidth; 47467 }, 47468 set: function set$$1(bandwidth) { 47469 this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth; 47470 // setting the bandwidth manually resets the throughput counter 47471 // `count` is set to zero that current value of `rate` isn't included 47472 // in the cumulative average 47473 this.masterPlaylistController_.mainSegmentLoader_.throughput = { 47474 rate: 0, 47475 count: 0 47476 }; 47477 } 47478 }, 47479 /** 47480 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 47481 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 47482 * the entire process after that - decryption, transmuxing, and appending - provided 47483 * by `throughput`. 47484 * 47485 * Since the two process are serial, the overall system bandwidth is given by: 47486 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 47487 */ 47488 systemBandwidth: { 47489 get: function get$$1() { 47490 var invBandwidth = 1 / (this.bandwidth || 1); 47491 var invThroughput = void 0; 47492 47493 if (this.throughput > 0) { 47494 invThroughput = 1 / this.throughput; 47495 } else { 47496 invThroughput = 0; 47497 } 47498 47499 var systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 47500 47501 return systemBitrate; 47502 }, 47503 set: function set$$1() { 47504 videojs$1.log.error('The "systemBandwidth" property is read-only'); 47505 } 47506 } 47507 }); 47508 47509 Object.defineProperties(this.stats, { 47510 bandwidth: { 47511 get: function get$$1() { 47512 return _this3.bandwidth || 0; 47513 }, 47514 enumerable: true 47515 }, 47516 mediaRequests: { 47517 get: function get$$1() { 47518 return _this3.masterPlaylistController_.mediaRequests_() || 0; 47519 }, 47520 enumerable: true 47521 }, 47522 mediaRequestsAborted: { 47523 get: function get$$1() { 47524 return _this3.masterPlaylistController_.mediaRequestsAborted_() || 0; 47525 }, 47526 enumerable: true 47527 }, 47528 mediaRequestsTimedout: { 47529 get: function get$$1() { 47530 return _this3.masterPlaylistController_.mediaRequestsTimedout_() || 0; 47531 }, 47532 enumerable: true 47533 }, 47534 mediaRequestsErrored: { 47535 get: function get$$1() { 47536 return _this3.masterPlaylistController_.mediaRequestsErrored_() || 0; 47537 }, 47538 enumerable: true 47539 }, 47540 mediaTransferDuration: { 47541 get: function get$$1() { 47542 return _this3.masterPlaylistController_.mediaTransferDuration_() || 0; 47543 }, 47544 enumerable: true 47545 }, 47546 mediaBytesTransferred: { 47547 get: function get$$1() { 47548 return _this3.masterPlaylistController_.mediaBytesTransferred_() || 0; 47549 }, 47550 enumerable: true 47551 }, 47552 mediaSecondsLoaded: { 47553 get: function get$$1() { 47554 return _this3.masterPlaylistController_.mediaSecondsLoaded_() || 0; 47555 }, 47556 enumerable: true 47557 }, 47558 buffered: { 47559 get: function get$$1() { 47560 return timeRangesToArray(_this3.tech_.buffered()); 47561 }, 47562 enumerable: true 47563 }, 47564 currentTime: { 47565 get: function get$$1() { 47566 return _this3.tech_.currentTime(); 47567 }, 47568 enumerable: true 47569 }, 47570 currentSource: { 47571 get: function get$$1() { 47572 return _this3.tech_.currentSource_; 47573 }, 47574 enumerable: true 47575 }, 47576 currentTech: { 47577 get: function get$$1() { 47578 return _this3.tech_.name_; 47579 }, 47580 enumerable: true 47581 }, 47582 duration: { 47583 get: function get$$1() { 47584 return _this3.tech_.duration(); 47585 }, 47586 enumerable: true 47587 }, 47588 master: { 47589 get: function get$$1() { 47590 return _this3.playlists.master; 47591 }, 47592 enumerable: true 47593 }, 47594 playerDimensions: { 47595 get: function get$$1() { 47596 return _this3.tech_.currentDimensions(); 47597 }, 47598 enumerable: true 47599 }, 47600 seekable: { 47601 get: function get$$1() { 47602 return timeRangesToArray(_this3.tech_.seekable()); 47603 }, 47604 enumerable: true 47605 }, 47606 timestamp: { 47607 get: function get$$1() { 47608 return Date.now(); 47609 }, 47610 enumerable: true 47611 }, 47612 videoPlaybackQuality: { 47613 get: function get$$1() { 47614 return _this3.tech_.getVideoPlaybackQuality(); 47615 }, 47616 enumerable: true 47617 } 47618 }); 47619 47620 this.tech_.one('canplay', this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_)); 47621 47622 this.masterPlaylistController_.on('selectedinitialmedia', function () { 47623 // Add the manual rendition mix-in to HlsHandler 47624 renditionSelectionMixin(_this3); 47625 setupEmeOptions(_this3); 47626 }); 47627 47628 // the bandwidth of the primary segment loader is our best 47629 // estimate of overall bandwidth 47630 this.on(this.masterPlaylistController_, 'progress', function () { 47631 this.tech_.trigger('progress'); 47632 }); 47633 47634 // In the live case, we need to ignore the very first `seeking` event since 47635 // that will be the result of the seek-to-live behavior 47636 this.on(this.masterPlaylistController_, 'firstplay', function () { 47637 this.ignoreNextSeekingEvent_ = true; 47638 }); 47639 47640 this.tech_.ready(function () { 47641 return _this3.setupQualityLevels_(); 47642 }); 47643 47644 // do nothing if the tech has been disposed already 47645 // this can occur if someone sets the src in player.ready(), for instance 47646 if (!this.tech_.el()) { 47647 return; 47648 } 47649 47650 this.tech_.src(videojs$1.URL.createObjectURL(this.masterPlaylistController_.mediaSource)); 47651 } 47652 47653 /** 47654 * Initializes the quality levels and sets listeners to update them. 47655 * 47656 * @method setupQualityLevels_ 47657 * @private 47658 */ 47659 47660 }, { 47661 key: 'setupQualityLevels_', 47662 value: function setupQualityLevels_() { 47663 var _this4 = this; 47664 47665 var player = videojs$1.players[this.tech_.options_.playerId]; 47666 47667 if (player && player.qualityLevels) { 47668 this.qualityLevels_ = player.qualityLevels(); 47669 47670 this.masterPlaylistController_.on('selectedinitialmedia', function () { 47671 handleHlsLoadedMetadata(_this4.qualityLevels_, _this4); 47672 }); 47673 47674 this.playlists.on('mediachange', function () { 47675 handleHlsMediaChange(_this4.qualityLevels_, _this4.playlists); 47676 }); 47677 } 47678 } 47679 47680 /** 47681 * Begin playing the video. 47682 */ 47683 47684 }, { 47685 key: 'play', 47686 value: function play() { 47687 this.masterPlaylistController_.play(); 47688 } 47689 47690 /** 47691 * a wrapper around the function in MasterPlaylistController 47692 */ 47693 47694 }, { 47695 key: 'setCurrentTime', 47696 value: function setCurrentTime(currentTime) { 47697 this.masterPlaylistController_.setCurrentTime(currentTime); 47698 } 47699 47700 /** 47701 * a wrapper around the function in MasterPlaylistController 47702 */ 47703 47704 }, { 47705 key: 'duration', 47706 value: function duration$$1() { 47707 return this.masterPlaylistController_.duration(); 47708 } 47709 47710 /** 47711 * a wrapper around the function in MasterPlaylistController 47712 */ 47713 47714 }, { 47715 key: 'seekable', 47716 value: function seekable$$1() { 47717 return this.masterPlaylistController_.seekable(); 47718 } 47719 47720 /** 47721 * Abort all outstanding work and cleanup. 47722 */ 47723 47724 }, { 47725 key: 'dispose', 47726 value: function dispose() { 47727 if (this.playbackWatcher_) { 47728 this.playbackWatcher_.dispose(); 47729 } 47730 if (this.masterPlaylistController_) { 47731 this.masterPlaylistController_.dispose(); 47732 } 47733 if (this.qualityLevels_) { 47734 this.qualityLevels_.dispose(); 47735 } 47736 get$2(HlsHandler.prototype.__proto__ || Object.getPrototypeOf(HlsHandler.prototype), 'dispose', this).call(this); 47737 } 47738 }]); 47739 return HlsHandler; 47740 }(Component$1); 47741 47742 /** 47743 * The Source Handler object, which informs video.js what additional 47744 * MIME types are supported and sets up playback. It is registered 47745 * automatically to the appropriate tech based on the capabilities of 47746 * the browser it is running in. It is not necessary to use or modify 47747 * this object in normal usage. 47748 */ 47749 47750 var HlsSourceHandler = { 47751 name: 'videojs-http-streaming', 47752 VERSION: version$3, 47753 canHandleSource: function canHandleSource(srcObj) { 47754 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 47755 47756 var localOptions = videojs$1.mergeOptions(videojs$1.options, options); 47757 47758 return HlsSourceHandler.canPlayType(srcObj.type, localOptions); 47759 }, 47760 handleSource: function handleSource(source, tech) { 47761 var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 47762 47763 var localOptions = videojs$1.mergeOptions(videojs$1.options, options); 47764 47765 tech.hls = new HlsHandler(source, tech, localOptions); 47766 tech.hls.xhr = xhrFactory(); 47767 47768 tech.hls.src(source.src, source.type); 47769 return tech.hls; 47770 }, 47771 canPlayType: function canPlayType(type) { 47772 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 47773 47774 var _videojs$mergeOptions = videojs$1.mergeOptions(videojs$1.options, options), 47775 overrideNative = _videojs$mergeOptions.hls.overrideNative; 47776 47777 var supportedType = simpleTypeFromSourceType(type); 47778 var canUseMsePlayback = supportedType && (!Hls$1.supportsTypeNatively(supportedType) || overrideNative); 47779 47780 return canUseMsePlayback ? 'maybe' : ''; 47781 } 47782 }; 47783 47784 if (typeof videojs$1.MediaSource === 'undefined' || typeof videojs$1.URL === 'undefined') { 47785 videojs$1.MediaSource = MediaSource; 47786 videojs$1.URL = URL$1; 47787 } 47788 47789 // register source handlers with the appropriate techs 47790 if (MediaSource.supportsNativeMediaSources()) { 47791 videojs$1.getTech('Html5').registerSourceHandler(HlsSourceHandler, 0); 47792 } 47793 47794 videojs$1.HlsHandler = HlsHandler; 47795 videojs$1.HlsSourceHandler = HlsSourceHandler; 47796 videojs$1.Hls = Hls$1; 47797 if (!videojs$1.use) { 47798 videojs$1.registerComponent('Hls', Hls$1); 47799 } 47800 videojs$1.options.hls = videojs$1.options.hls || {}; 47801 47802 if (videojs$1.registerPlugin) { 47803 videojs$1.registerPlugin('reloadSourceOnError', reloadSourceOnError); 47804 } else { 47805 videojs$1.plugin('reloadSourceOnError', reloadSourceOnError); 47806 } 47807 47808 return videojs$1; 47809 47810})));
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.