1/*! @name @videojs/http-streaming @version 3.17.2
vendor: 8,545 bytes, lines 1-297
1 @license Apache-2.0 */ 2(function (global, factory) { 3 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('video.js'), require('@xmldom/xmldom')) : 4 typeof define === 'function' && define.amd ? define(['exports', 'video.js', '@xmldom/xmldom'], factory) : 5 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.httpStreaming = {}, global.videojs, global.window)); 6})(this, (function (exports, videojs, xmldom) { 'use strict'; 7 8 function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 9 10 var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); 11 12 function _extends() { 13 _extends = Object.assign || function (target) { 14 for (var i = 1; i < arguments.length; i++) { 15 var source = arguments[i]; 16 17 for (var key in source) { 18 if (Object.prototype.hasOwnProperty.call(source, key)) { 19 target[key] = source[key]; 20 } 21 } 22 } 23 24 return target; 25 }; 26 27 return _extends.apply(this, arguments); 28 } 29 30 var DEFAULT_LOCATION = 'https://example.com'; 31 32 var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) { 33 // return early if we don't need to resolve 34 if (/^[a-z]+:/i.test(relativeUrl)) { 35 return relativeUrl; 36 } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location 37 38 39 if (/^data:/.test(baseUrl)) { 40 baseUrl = window.location && window.location.href || ''; 41 } 42 43 var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node) 44 // and if baseUrl isn't an absolute url 45 46 var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location 47 48 baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION); 49 var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol 50 // and if we're location-less, remove the location 51 // otherwise, return the url unmodified 52 53 if (removeLocation) { 54 return newUrl.href.slice(DEFAULT_LOCATION.length); 55 } else if (protocolLess) { 56 return newUrl.href.slice(newUrl.protocol.length); 57 } 58 59 return newUrl.href; 60 }; 61 62 /** 63 * @file resolve-url.js - Handling how URLs are resolved and manipulated 64 */ 65 const resolveUrl = resolveUrl$1; 66 /** 67 * If the xhr request was redirected, return the responseURL, otherwise, 68 * return the original url. 69 * 70 * @api private 71 * 72 * @param {string} url - an url being requested 73 * @param {XMLHttpRequest} req - xhr request result 74 * 75 * @return {string} 76 */ 77 78 const resolveManifestRedirect = (url, req) => { 79 // To understand how the responseURL below is set and generated: 80 // - https://fetch.spec.whatwg.org/#concept-response-url 81 // - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 82 if (req && req.responseURL && url !== req.responseURL) { 83 return req.responseURL; 84 } 85 86 return url; 87 }; 88 89 const logger = source => { 90 if (videojs__default["default"].log.debug) { 91 return videojs__default["default"].log.debug.bind(videojs__default["default"], 'VHS:', `${source} >`); 92 } 93 94 return function () {}; 95 }; 96 97 /** 98 * @file stream.js 99 */ 100 101 /** 102 * A lightweight readable stream implemention that handles event dispatching. 103 * 104 * @class Stream 105 */ 106 var Stream = /*#__PURE__*/function () { 107 function Stream() { 108 this.listeners = {}; 109 } 110 /** 111 * Add a listener for a specified event type. 112 * 113 * @param {string} type the event name 114 * @param {Function} listener the callback to be invoked when an event of 115 * the specified type occurs 116 */ 117 118 119 var _proto = Stream.prototype; 120 121 _proto.on = function on(type, listener) { 122 if (!this.listeners[type]) { 123 this.listeners[type] = []; 124 } 125 126 this.listeners[type].push(listener); 127 } 128 /** 129 * Remove a listener for a specified event type. 130 * 131 * @param {string} type the event name 132 * @param {Function} listener a function previously registered for this 133 * type of event through `on` 134 * @return {boolean} if we could turn it off or not 135 */ 136 ; 137 138 _proto.off = function off(type, listener) { 139 if (!this.listeners[type]) { 140 return false; 141 } 142 143 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 144 // In Video.js we slice listener functions 145 // on trigger so that it does not mess up the order 146 // while we loop through. 147 // 148 // Here we slice on off so that the loop in trigger 149 // can continue using it's old reference to loop without 150 // messing up the order. 151 152 this.listeners[type] = this.listeners[type].slice(0); 153 this.listeners[type].splice(index, 1); 154 return index > -1; 155 } 156 /** 157 * Trigger an event of the specified type on this stream. Any additional 158 * arguments to this function are passed as parameters to event listeners. 159 * 160 * @param {string} type the event name 161 */ 162 ; 163 164 _proto.trigger = function trigger(type) { 165 var callbacks = this.listeners[type]; 166 167 if (!callbacks) { 168 return; 169 } // Slicing the arguments on every invocation of this method 170 // can add a significant amount of overhead. Avoid the 171 // intermediate object creation for the common case of a 172 // single callback argument 173 174 175 if (arguments.length === 2) { 176 var length = callbacks.length; 177 178 for (var i = 0; i < length; ++i) { 179 callbacks[i].call(this, arguments[1]); 180 } 181 } else { 182 var args = Array.prototype.slice.call(arguments, 1); 183 var _length = callbacks.length; 184 185 for (var _i = 0; _i < _length; ++_i) { 186 callbacks[_i].apply(this, args); 187 } 188 } 189 } 190 /** 191 * Destroys the stream and cleans up. 192 */ 193 ; 194 195 _proto.dispose = function dispose() { 196 this.listeners = {}; 197 } 198 /** 199 * Forwards all `data` events on this stream to the destination stream. The 200 * destination stream should provide a method `push` to receive the data 201 * events as they arrive. 202 * 203 * @param {Stream} destination the stream that will receive all `data` events 204 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 205 */ 206 ; 207 208 _proto.pipe = function pipe(destination) { 209 this.on('data', function (data) { 210 destination.push(data); 211 }); 212 }; 213 214 return Stream; 215 }(); 216 217 var atob = function atob(s) { 218 return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary'); 219 }; 220 221 function decodeB64ToUint8Array(b64Text) { 222 var decodedString = atob(b64Text); 223 var array = new Uint8Array(decodedString.length); 224 225 for (var i = 0; i < decodedString.length; i++) { 226 array[i] = decodedString.charCodeAt(i); 227 } 228 229 return array; 230 } 231 232 /*! @name m3u8-parser @version 7.2.0 @license Apache-2.0 */ 233 /** 234 * @file m3u8/line-stream.js 235 */ 236 237 /** 238 * A stream that buffers string input and generates a `data` event for each 239 * line. 240 * 241 * @class LineStream 242 * @extends Stream 243 */ 244 245 class LineStream extends Stream { 246 constructor() { 247 super(); 248 this.buffer = ''; 249 } 250 /** 251 * Add new data to be parsed. 252 * 253 * @param {string} data the text to process 254 */ 255 256 257 push(data) { 258 let nextNewline; 259 this.buffer += data; 260 nextNewline = this.buffer.indexOf('\n'); 261 262 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { 263 this.trigger('data', this.buffer.substring(0, nextNewline)); 264 this.buffer = this.buffer.substring(nextNewline + 1); 265 } 266 } 267 268 } 269 270 const TAB = String.fromCharCode(0x09); 271 272 const parseByterange = function (byterangeString) { 273 // optionally match and capture 0+ digits before `@` 274 // optionally match and capture 0+ digits after `@` 275 const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || ''); 276 const result = {}; 277 278 if (match[1]) { 279 result.length = parseInt(match[1], 10); 280 } 281 282 if (match[2]) { 283 result.offset = parseInt(match[2], 10); 284 } 285 286 return result; 287 }; 288 /** 289 * "forgiving" attribute list psuedo-grammar: 290 * attributes -> keyvalue (',' keyvalue)* 291 * keyvalue -> key '=' value 292 * key -> [^=]* 293 * value -> '"' [^"]* '"' | [^,]* 294 */ 295 296 297 const attributeSeparator = function () {
vendor: 64,075 bytes, lines 298-2194
298 const key = '[^=]*'; 299 const value = '"[^"]*"|[^,]*'; 300 const keyvalue = '(?:' + key + ')=(?:' + value + ')'; 301 return new RegExp('(?:^|,)(' + keyvalue + ')'); 302 }; 303 /** 304 * Parse attributes from a line given the separator 305 * 306 * @param {string} attributes the attribute line to parse 307 */ 308 309 310 const parseAttributes$1 = function (attributes) { 311 const result = {}; 312 313 if (!attributes) { 314 return result; 315 } // split the string using attributes as the separator 316 317 318 const attrs = attributes.split(attributeSeparator()); 319 let i = attrs.length; 320 let attr; 321 322 while (i--) { 323 // filter out unmatched portions of the string 324 if (attrs[i] === '') { 325 continue; 326 } // split the key and value 327 328 329 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value 330 331 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 332 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 333 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 334 result[attr[0]] = attr[1]; 335 } 336 337 return result; 338 }; 339 /** 340 * Converts a string into a resolution object 341 * 342 * @param {string} resolution a string such as 3840x2160 343 * 344 * @return {Object} An object representing the resolution 345 * 346 */ 347 348 349 const parseResolution = resolution => { 350 const split = resolution.split('x'); 351 const result = {}; 352 353 if (split[0]) { 354 result.width = parseInt(split[0], 10); 355 } 356 357 if (split[1]) { 358 result.height = parseInt(split[1], 10); 359 } 360 361 return result; 362 }; 363 /** 364 * A line-level M3U8 parser event stream. It expects to receive input one 365 * line at a time and performs a context-free parse of its contents. A stream 366 * interpretation of a manifest can be useful if the manifest is expected to 367 * be too large to fit comfortably into memory or the entirety of the input 368 * is not immediately available. Otherwise, it's probably much easier to work 369 * with a regular `Parser` object. 370 * 371 * Produces `data` events with an object that captures the parser's 372 * interpretation of the input. That object has a property `tag` that is one 373 * of `uri`, `comment`, or `tag`. URIs only have a single additional 374 * property, `line`, which captures the entirety of the input without 375 * interpretation. Comments similarly have a single additional property 376 * `text` which is the input without the leading `#`. 377 * 378 * Tags always have a property `tagType` which is the lower-cased version of 379 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, 380 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 381 * tags are given the tag type `unknown` and a single additional property 382 * `data` with the remainder of the input. 383 * 384 * @class ParseStream 385 * @extends Stream 386 */ 387 388 389 class ParseStream extends Stream { 390 constructor() { 391 super(); 392 this.customParsers = []; 393 this.tagMappers = []; 394 } 395 /** 396 * Parses an additional line of input. 397 * 398 * @param {string} line a single line of an M3U8 file to parse 399 */ 400 401 402 push(line) { 403 let match; 404 let event; // strip whitespace 405 406 line = line.trim(); 407 408 if (line.length === 0) { 409 // ignore empty lines 410 return; 411 } // URIs 412 413 414 if (line[0] !== '#') { 415 this.trigger('data', { 416 type: 'uri', 417 uri: line 418 }); 419 return; 420 } // map tags 421 422 423 const newLines = this.tagMappers.reduce((acc, mapper) => { 424 const mappedLine = mapper(line); // skip if unchanged 425 426 if (mappedLine === line) { 427 return acc; 428 } 429 430 return acc.concat([mappedLine]); 431 }, [line]); 432 newLines.forEach(newLine => { 433 for (let i = 0; i < this.customParsers.length; i++) { 434 if (this.customParsers[i].call(this, newLine)) { 435 return; 436 } 437 } // Comments 438 439 440 if (newLine.indexOf('#EXT') !== 0) { 441 this.trigger('data', { 442 type: 'comment', 443 text: newLine.slice(1) 444 }); 445 return; 446 } // strip off any carriage returns here so the regex matching 447 // doesn't have to account for them. 448 449 450 newLine = newLine.replace('\r', ''); // Tags 451 452 match = /^#EXTM3U/.exec(newLine); 453 454 if (match) { 455 this.trigger('data', { 456 type: 'tag', 457 tagType: 'm3u' 458 }); 459 return; 460 } 461 462 match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine); 463 464 if (match) { 465 event = { 466 type: 'tag', 467 tagType: 'inf' 468 }; 469 470 if (match[1]) { 471 event.duration = parseFloat(match[1]); 472 } 473 474 if (match[2]) { 475 event.title = match[2]; 476 } 477 478 this.trigger('data', event); 479 return; 480 } 481 482 match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine); 483 484 if (match) { 485 event = { 486 type: 'tag', 487 tagType: 'targetduration' 488 }; 489 490 if (match[1]) { 491 event.duration = parseInt(match[1], 10); 492 } 493 494 this.trigger('data', event); 495 return; 496 } 497 498 match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine); 499 500 if (match) { 501 event = { 502 type: 'tag', 503 tagType: 'version' 504 }; 505 506 if (match[1]) { 507 event.version = parseInt(match[1], 10); 508 } 509 510 this.trigger('data', event); 511 return; 512 } 513 514 match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine); 515 516 if (match) { 517 event = { 518 type: 'tag', 519 tagType: 'media-sequence' 520 }; 521 522 if (match[1]) { 523 event.number = parseInt(match[1], 10); 524 } 525 526 this.trigger('data', event); 527 return; 528 } 529 530 match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine); 531 532 if (match) { 533 event = { 534 type: 'tag', 535 tagType: 'discontinuity-sequence' 536 }; 537 538 if (match[1]) { 539 event.number = parseInt(match[1], 10); 540 } 541 542 this.trigger('data', event); 543 return; 544 } 545 546 match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine); 547 548 if (match) { 549 event = { 550 type: 'tag', 551 tagType: 'playlist-type' 552 }; 553 554 if (match[1]) { 555 event.playlistType = match[1]; 556 } 557 558 this.trigger('data', event); 559 return; 560 } 561 562 match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine); 563 564 if (match) { 565 event = _extends(parseByterange(match[1]), { 566 type: 'tag', 567 tagType: 'byterange' 568 }); 569 this.trigger('data', event); 570 return; 571 } 572 573 match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine); 574 575 if (match) { 576 event = { 577 type: 'tag', 578 tagType: 'allow-cache' 579 }; 580 581 if (match[1]) { 582 event.allowed = !/NO/.test(match[1]); 583 } 584 585 this.trigger('data', event); 586 return; 587 } 588 589 match = /^#EXT-X-MAP:(.*)$/.exec(newLine); 590 591 if (match) { 592 event = { 593 type: 'tag', 594 tagType: 'map' 595 }; 596 597 if (match[1]) { 598 const attributes = parseAttributes$1(match[1]); 599 600 if (attributes.URI) { 601 event.uri = attributes.URI; 602 } 603 604 if (attributes.BYTERANGE) { 605 event.byterange = parseByterange(attributes.BYTERANGE); 606 } 607 } 608 609 this.trigger('data', event); 610 return; 611 } 612 613 match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine); 614 615 if (match) { 616 event = { 617 type: 'tag', 618 tagType: 'stream-inf' 619 }; 620 621 if (match[1]) { 622 event.attributes = parseAttributes$1(match[1]); 623 624 if (event.attributes.RESOLUTION) { 625 event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION); 626 } 627 628 if (event.attributes.BANDWIDTH) { 629 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 630 } 631 632 if (event.attributes['FRAME-RATE']) { 633 event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']); 634 } 635 636 if (event.attributes['PROGRAM-ID']) { 637 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); 638 } 639 } 640 641 this.trigger('data', event); 642 return; 643 } 644 645 match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine); 646 647 if (match) { 648 event = { 649 type: 'tag', 650 tagType: 'media' 651 }; 652 653 if (match[1]) { 654 event.attributes = parseAttributes$1(match[1]); 655 } 656 657 this.trigger('data', event); 658 return; 659 } 660 661 match = /^#EXT-X-ENDLIST/.exec(newLine); 662 663 if (match) { 664 this.trigger('data', { 665 type: 'tag', 666 tagType: 'endlist' 667 }); 668 return; 669 } 670 671 match = /^#EXT-X-DISCONTINUITY/.exec(newLine); 672 673 if (match) { 674 this.trigger('data', { 675 type: 'tag', 676 tagType: 'discontinuity' 677 }); 678 return; 679 } 680 681 match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine); 682 683 if (match) { 684 event = { 685 type: 'tag', 686 tagType: 'program-date-time' 687 }; 688 689 if (match[1]) { 690 event.dateTimeString = match[1]; 691 event.dateTimeObject = new Date(match[1]); 692 } 693 694 this.trigger('data', event); 695 return; 696 } 697 698 match = /^#EXT-X-KEY:(.*)$/.exec(newLine); 699 700 if (match) { 701 event = { 702 type: 'tag', 703 tagType: 'key' 704 }; 705 706 if (match[1]) { 707 event.attributes = parseAttributes$1(match[1]); // parse the IV string into a Uint32Array 708 709 if (event.attributes.IV) { 710 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { 711 event.attributes.IV = event.attributes.IV.substring(2); 712 } 713 714 event.attributes.IV = event.attributes.IV.match(/.{8}/g); 715 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); 716 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); 717 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); 718 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); 719 event.attributes.IV = new Uint32Array(event.attributes.IV); 720 } 721 } 722 723 this.trigger('data', event); 724 return; 725 } 726 727 match = /^#EXT-X-START:(.*)$/.exec(newLine); 728 729 if (match) { 730 event = { 731 type: 'tag', 732 tagType: 'start' 733 }; 734 735 if (match[1]) { 736 event.attributes = parseAttributes$1(match[1]); 737 event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); 738 event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); 739 } 740 741 this.trigger('data', event); 742 return; 743 } 744 745 match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine); 746 747 if (match) { 748 event = { 749 type: 'tag', 750 tagType: 'cue-out-cont' 751 }; 752 753 if (match[1]) { 754 event.data = match[1]; 755 } else { 756 event.data = ''; 757 } 758 759 this.trigger('data', event); 760 return; 761 } 762 763 match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine); 764 765 if (match) { 766 event = { 767 type: 'tag', 768 tagType: 'cue-out' 769 }; 770 771 if (match[1]) { 772 event.data = match[1]; 773 } else { 774 event.data = ''; 775 } 776 777 this.trigger('data', event); 778 return; 779 } 780 781 match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(newLine); 782 783 if (match) { 784 event = { 785 type: 'tag', 786 tagType: 'cue-in' 787 }; 788 789 if (match[1]) { 790 event.data = match[1]; 791 } else { 792 event.data = ''; 793 } 794 795 this.trigger('data', event); 796 return; 797 } 798 799 match = /^#EXT-X-SKIP:(.*)$/.exec(newLine); 800 801 if (match && match[1]) { 802 event = { 803 type: 'tag', 804 tagType: 'skip' 805 }; 806 event.attributes = parseAttributes$1(match[1]); 807 808 if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) { 809 event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10); 810 } 811 812 if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) { 813 event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB); 814 } 815 816 this.trigger('data', event); 817 return; 818 } 819 820 match = /^#EXT-X-PART:(.*)$/.exec(newLine); 821 822 if (match && match[1]) { 823 event = { 824 type: 'tag', 825 tagType: 'part' 826 }; 827 event.attributes = parseAttributes$1(match[1]); 828 ['DURATION'].forEach(function (key) { 829 if (event.attributes.hasOwnProperty(key)) { 830 event.attributes[key] = parseFloat(event.attributes[key]); 831 } 832 }); 833 ['INDEPENDENT', 'GAP'].forEach(function (key) { 834 if (event.attributes.hasOwnProperty(key)) { 835 event.attributes[key] = /YES/.test(event.attributes[key]); 836 } 837 }); 838 839 if (event.attributes.hasOwnProperty('BYTERANGE')) { 840 event.attributes.byterange = parseByterange(event.attributes.BYTERANGE); 841 } 842 843 this.trigger('data', event); 844 return; 845 } 846 847 match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine); 848 849 if (match && match[1]) { 850 event = { 851 type: 'tag', 852 tagType: 'server-control' 853 }; 854 event.attributes = parseAttributes$1(match[1]); 855 ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) { 856 if (event.attributes.hasOwnProperty(key)) { 857 event.attributes[key] = parseFloat(event.attributes[key]); 858 } 859 }); 860 ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) { 861 if (event.attributes.hasOwnProperty(key)) { 862 event.attributes[key] = /YES/.test(event.attributes[key]); 863 } 864 }); 865 this.trigger('data', event); 866 return; 867 } 868 869 match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine); 870 871 if (match && match[1]) { 872 event = { 873 type: 'tag', 874 tagType: 'part-inf' 875 }; 876 event.attributes = parseAttributes$1(match[1]); 877 ['PART-TARGET'].forEach(function (key) { 878 if (event.attributes.hasOwnProperty(key)) { 879 event.attributes[key] = parseFloat(event.attributes[key]); 880 } 881 }); 882 this.trigger('data', event); 883 return; 884 } 885 886 match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine); 887 888 if (match && match[1]) { 889 event = { 890 type: 'tag', 891 tagType: 'preload-hint' 892 }; 893 event.attributes = parseAttributes$1(match[1]); 894 ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) { 895 if (event.attributes.hasOwnProperty(key)) { 896 event.attributes[key] = parseInt(event.attributes[key], 10); 897 const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset'; 898 event.attributes.byterange = event.attributes.byterange || {}; 899 event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object. 900 901 delete event.attributes[key]; 902 } 903 }); 904 this.trigger('data', event); 905 return; 906 } 907 908 match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine); 909 910 if (match && match[1]) { 911 event = { 912 type: 'tag', 913 tagType: 'rendition-report' 914 }; 915 event.attributes = parseAttributes$1(match[1]); 916 ['LAST-MSN', 'LAST-PART'].forEach(function (key) { 917 if (event.attributes.hasOwnProperty(key)) { 918 event.attributes[key] = parseInt(event.attributes[key], 10); 919 } 920 }); 921 this.trigger('data', event); 922 return; 923 } 924 925 match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine); 926 927 if (match && match[1]) { 928 event = { 929 type: 'tag', 930 tagType: 'daterange' 931 }; 932 event.attributes = parseAttributes$1(match[1]); 933 ['ID', 'CLASS'].forEach(function (key) { 934 if (event.attributes.hasOwnProperty(key)) { 935 event.attributes[key] = String(event.attributes[key]); 936 } 937 }); 938 ['START-DATE', 'END-DATE'].forEach(function (key) { 939 if (event.attributes.hasOwnProperty(key)) { 940 event.attributes[key] = new Date(event.attributes[key]); 941 } 942 }); 943 ['DURATION', 'PLANNED-DURATION'].forEach(function (key) { 944 if (event.attributes.hasOwnProperty(key)) { 945 event.attributes[key] = parseFloat(event.attributes[key]); 946 } 947 }); 948 ['END-ON-NEXT'].forEach(function (key) { 949 if (event.attributes.hasOwnProperty(key)) { 950 event.attributes[key] = /YES/i.test(event.attributes[key]); 951 } 952 }); 953 ['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) { 954 if (event.attributes.hasOwnProperty(key)) { 955 event.attributes[key] = event.attributes[key].toString(16); 956 } 957 }); 958 const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/; 959 960 for (const key in event.attributes) { 961 if (!clientAttributePattern.test(key)) { 962 continue; 963 } 964 965 const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]); 966 const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]); 967 event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]); 968 } 969 970 this.trigger('data', event); 971 return; 972 } 973 974 match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine); 975 976 if (match) { 977 this.trigger('data', { 978 type: 'tag', 979 tagType: 'independent-segments' 980 }); 981 return; 982 } 983 984 match = /^#EXT-X-I-FRAMES-ONLY/.exec(newLine); 985 986 if (match) { 987 this.trigger('data', { 988 type: 'tag', 989 tagType: 'i-frames-only' 990 }); 991 return; 992 } 993 994 match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine); 995 996 if (match) { 997 event = { 998 type: 'tag', 999 tagType: 'content-steering' 1000 }; 1001 event.attributes = parseAttributes$1(match[1]); 1002 this.trigger('data', event); 1003 return; 1004 } 1005 1006 match = /^#EXT-X-I-FRAME-STREAM-INF:(.*)$/.exec(newLine); 1007 1008 if (match) { 1009 event = { 1010 type: 'tag', 1011 tagType: 'i-frame-playlist' 1012 }; 1013 event.attributes = parseAttributes$1(match[1]); 1014 1015 if (event.attributes.URI) { 1016 event.uri = event.attributes.URI; 1017 } 1018 1019 if (event.attributes.BANDWIDTH) { 1020 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 1021 } 1022 1023 if (event.attributes.RESOLUTION) { 1024 event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION); 1025 } 1026 1027 if (event.attributes['AVERAGE-BANDWIDTH']) { 1028 event.attributes['AVERAGE-BANDWIDTH'] = parseInt(event.attributes['AVERAGE-BANDWIDTH'], 10); 1029 } 1030 1031 if (event.attributes['FRAME-RATE']) { 1032 event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']); 1033 } 1034 1035 this.trigger('data', event); 1036 return; 1037 } 1038 1039 match = /^#EXT-X-DEFINE:(.*)$/.exec(newLine); 1040 1041 if (match) { 1042 event = { 1043 type: 'tag', 1044 tagType: 'define' 1045 }; 1046 event.attributes = parseAttributes$1(match[1]); 1047 this.trigger('data', event); 1048 return; 1049 } // unknown tag type 1050 1051 1052 this.trigger('data', { 1053 type: 'tag', 1054 data: newLine.slice(4) 1055 }); 1056 }); 1057 } 1058 /** 1059 * Add a parser for custom headers 1060 * 1061 * @param {Object} options a map of options for the added parser 1062 * @param {RegExp} options.expression a regular expression to match the custom header 1063 * @param {string} options.customType the custom type to register to the output 1064 * @param {Function} [options.dataParser] function to parse the line into an object 1065 * @param {boolean} [options.segment] should tag data be attached to the segment object 1066 */ 1067 1068 1069 addParser({ 1070 expression, 1071 customType, 1072 dataParser, 1073 segment 1074 }) { 1075 if (typeof dataParser !== 'function') { 1076 dataParser = line => line; 1077 } 1078 1079 this.customParsers.push(line => { 1080 const match = expression.exec(line); 1081 1082 if (match) { 1083 this.trigger('data', { 1084 type: 'custom', 1085 data: dataParser(line), 1086 customType, 1087 segment 1088 }); 1089 return true; 1090 } 1091 }); 1092 } 1093 /** 1094 * Add a custom header mapper 1095 * 1096 * @param {Object} options 1097 * @param {RegExp} options.expression a regular expression to match the custom header 1098 * @param {Function} options.map function to translate tag into a different tag 1099 */ 1100 1101 1102 addTagMapper({ 1103 expression, 1104 map 1105 }) { 1106 const mapFn = line => { 1107 if (expression.test(line)) { 1108 return map(line); 1109 } 1110 1111 return line; 1112 }; 1113 1114 this.tagMappers.push(mapFn); 1115 } 1116 1117 } 1118 1119 const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase()); 1120 1121 const camelCaseKeys = function (attributes) { 1122 const result = {}; 1123 Object.keys(attributes).forEach(function (key) { 1124 result[camelCase(key)] = attributes[key]; 1125 }); 1126 return result; 1127 }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration 1128 // we need this helper because defaults are based upon targetDuration and 1129 // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before 1130 // target durations are set. 1131 1132 1133 const setHoldBack = function (manifest) { 1134 const { 1135 serverControl, 1136 targetDuration, 1137 partTargetDuration 1138 } = manifest; 1139 1140 if (!serverControl) { 1141 return; 1142 } 1143 1144 const tag = '#EXT-X-SERVER-CONTROL'; 1145 const hb = 'holdBack'; 1146 const phb = 'partHoldBack'; 1147 const minTargetDuration = targetDuration && targetDuration * 3; 1148 const minPartDuration = partTargetDuration && partTargetDuration * 2; 1149 1150 if (targetDuration && !serverControl.hasOwnProperty(hb)) { 1151 serverControl[hb] = minTargetDuration; 1152 this.trigger('info', { 1153 message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).` 1154 }); 1155 } 1156 1157 if (minTargetDuration && serverControl[hb] < minTargetDuration) { 1158 this.trigger('warn', { 1159 message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})` 1160 }); 1161 serverControl[hb] = minTargetDuration; 1162 } // default no part hold back to part target duration * 3 1163 1164 1165 if (partTargetDuration && !serverControl.hasOwnProperty(phb)) { 1166 serverControl[phb] = partTargetDuration * 3; 1167 this.trigger('info', { 1168 message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).` 1169 }); 1170 } // if part hold back is too small default it to part target duration * 2 1171 1172 1173 if (partTargetDuration && serverControl[phb] < minPartDuration) { 1174 this.trigger('warn', { 1175 message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).` 1176 }); 1177 serverControl[phb] = minPartDuration; 1178 } 1179 }; 1180 /** 1181 * A parser for M3U8 files. The current interpretation of the input is 1182 * exposed as a property `manifest` on parser objects. It's just two lines to 1183 * create and parse a manifest once you have the contents available as a string: 1184 * 1185 * ```js 1186 * var parser = new m3u8.Parser(); 1187 * parser.push(xhr.responseText); 1188 * ``` 1189 * 1190 * New input can later be applied to update the manifest object by calling 1191 * `push` again. 1192 * 1193 * The parser attempts to create a usable manifest object even if the 1194 * underlying input is somewhat nonsensical. It emits `info` and `warning` 1195 * events during the parse if it encounters input that seems invalid or 1196 * requires some property of the manifest object to be defaulted. 1197 * 1198 * @class Parser 1199 * @param {Object} [opts] Options for the constructor, needed for substitutions 1200 * @param {string} [opts.uri] URL to check for query params 1201 * @param {Object} [opts.mainDefinitions] Definitions on main playlist that can be imported 1202 * @extends Stream 1203 */ 1204 1205 1206 class Parser extends Stream { 1207 constructor(opts = {}) { 1208 super(); 1209 this.lineStream = new LineStream(); 1210 this.parseStream = new ParseStream(); 1211 this.lineStream.pipe(this.parseStream); 1212 this.mainDefinitions = opts.mainDefinitions || {}; 1213 this.params = new URL(opts.uri, 'https://a.com').searchParams; 1214 this.lastProgramDateTime = null; 1215 /* eslint-disable consistent-this */ 1216 1217 const self = this; 1218 /* eslint-enable consistent-this */ 1219 1220 const uris = []; 1221 let currentUri = {}; // if specified, the active EXT-X-MAP definition 1222 1223 let currentMap; // if specified, the active decryption key 1224 1225 let key; 1226 let hasParts = false; 1227 1228 const noop = function () {}; 1229 1230 const defaultMediaGroups = { 1231 'AUDIO': {}, 1232 'VIDEO': {}, 1233 'CLOSED-CAPTIONS': {}, 1234 'SUBTITLES': {} 1235 }; // This is the Widevine UUID from DASH IF IOP. The same exact string is 1236 // used in MPDs with Widevine encrypted streams. 1237 1238 const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities 1239 1240 let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data 1241 1242 this.manifest = { 1243 allowCache: true, 1244 discontinuityStarts: [], 1245 dateRanges: [], 1246 iFramePlaylists: [], 1247 segments: [] 1248 }; // keep track of the last seen segment's byte range end, as segments are not required 1249 // to provide the offset, in which case it defaults to the next byte after the 1250 // previous segment 1251 1252 let lastByterangeEnd = 0; // keep track of the last seen part's byte range end. 1253 1254 let lastPartByterangeEnd = 0; 1255 const dateRangeTags = {}; 1256 this.on('end', () => { 1257 // only add preloadSegment if we don't yet have a uri for it. 1258 // and we actually have parts/preloadHints 1259 if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) { 1260 return; 1261 } 1262 1263 if (!currentUri.map && currentMap) { 1264 currentUri.map = currentMap; 1265 } 1266 1267 if (!currentUri.key && key) { 1268 currentUri.key = key; 1269 } 1270 1271 if (!currentUri.timeline && typeof currentTimeline === 'number') { 1272 currentUri.timeline = currentTimeline; 1273 } 1274 1275 this.manifest.preloadSegment = currentUri; 1276 }); // update the manifest with the m3u8 entry from the parse stream 1277 1278 this.parseStream.on('data', function (entry) { 1279 let mediaGroup; 1280 let rendition; // Replace variables in uris and attributes as defined in #EXT-X-DEFINE tags 1281 1282 if (self.manifest.definitions) { 1283 for (const def in self.manifest.definitions) { 1284 if (entry.uri) { 1285 entry.uri = entry.uri.replace(`{$${def}}`, self.manifest.definitions[def]); 1286 } 1287 1288 if (entry.attributes) { 1289 for (const attr in entry.attributes) { 1290 if (typeof entry.attributes[attr] === 'string') { 1291 entry.attributes[attr] = entry.attributes[attr].replace(`{$${def}}`, self.manifest.definitions[def]); 1292 } 1293 } 1294 } 1295 } 1296 } 1297 1298 ({ 1299 tag() { 1300 // switch based on the tag type 1301 (({ 1302 version() { 1303 if (entry.version) { 1304 this.manifest.version = entry.version; 1305 } 1306 }, 1307 1308 'allow-cache'() { 1309 this.manifest.allowCache = entry.allowed; 1310 1311 if (!('allowed' in entry)) { 1312 this.trigger('info', { 1313 message: 'defaulting allowCache to YES' 1314 }); 1315 this.manifest.allowCache = true; 1316 } 1317 }, 1318 1319 byterange() { 1320 const byterange = {}; 1321 1322 if ('length' in entry) { 1323 currentUri.byterange = byterange; 1324 byterange.length = entry.length; 1325 1326 if (!('offset' in entry)) { 1327 /* 1328 * From the latest spec (as of this writing): 1329 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2 1330 * 1331 * Same text since EXT-X-BYTERANGE's introduction in draft 7: 1332 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1) 1333 * 1334 * "If o [offset] is not present, the sub-range begins at the next byte 1335 * following the sub-range of the previous media segment." 1336 */ 1337 entry.offset = lastByterangeEnd; 1338 } 1339 } 1340 1341 if ('offset' in entry) { 1342 currentUri.byterange = byterange; 1343 byterange.offset = entry.offset; 1344 } 1345 1346 lastByterangeEnd = byterange.offset + byterange.length; 1347 }, 1348 1349 endlist() { 1350 this.manifest.endList = true; 1351 }, 1352 1353 inf() { 1354 if (!('mediaSequence' in this.manifest)) { 1355 this.manifest.mediaSequence = 0; 1356 this.trigger('info', { 1357 message: 'defaulting media sequence to zero' 1358 }); 1359 } 1360 1361 if (!('discontinuitySequence' in this.manifest)) { 1362 this.manifest.discontinuitySequence = 0; 1363 this.trigger('info', { 1364 message: 'defaulting discontinuity sequence to zero' 1365 }); 1366 } 1367 1368 if (entry.title) { 1369 currentUri.title = entry.title; 1370 } 1371 1372 if (entry.duration > 0) { 1373 currentUri.duration = entry.duration; 1374 } 1375 1376 if (entry.duration === 0) { 1377 currentUri.duration = 0.01; 1378 this.trigger('info', { 1379 message: 'updating zero segment duration to a small value' 1380 }); 1381 } 1382 1383 this.manifest.segments = uris; 1384 }, 1385 1386 key() { 1387 if (!entry.attributes) { 1388 this.trigger('warn', { 1389 message: 'ignoring key declaration without attribute list' 1390 }); 1391 return; 1392 } // clear the active encryption key 1393 1394 1395 if (entry.attributes.METHOD === 'NONE') { 1396 key = null; 1397 return; 1398 } 1399 1400 if (!entry.attributes.URI) { 1401 this.trigger('warn', { 1402 message: 'ignoring key declaration without URI' 1403 }); 1404 return; 1405 } 1406 1407 if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') { 1408 this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this. 1409 1410 this.manifest.contentProtection['com.apple.fps.1_0'] = { 1411 attributes: entry.attributes 1412 }; 1413 return; 1414 } 1415 1416 if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') { 1417 this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this. 1418 1419 this.manifest.contentProtection['com.microsoft.playready'] = { 1420 uri: entry.attributes.URI 1421 }; 1422 return; 1423 } // check if the content is encrypted for Widevine 1424 // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf 1425 1426 1427 if (entry.attributes.KEYFORMAT === widevineUuid) { 1428 const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC']; 1429 1430 if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) { 1431 this.trigger('warn', { 1432 message: 'invalid key method provided for Widevine' 1433 }); 1434 return; 1435 } 1436 1437 if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') { 1438 this.trigger('warn', { 1439 message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead' 1440 }); 1441 } 1442 1443 if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') { 1444 this.trigger('warn', { 1445 message: 'invalid key URI provided for Widevine' 1446 }); 1447 return; 1448 } 1449 1450 if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) { 1451 this.trigger('warn', { 1452 message: 'invalid key ID provided for Widevine' 1453 }); 1454 return; 1455 } // if Widevine key attributes are valid, store them as `contentProtection` 1456 // on the manifest to emulate Widevine tag structure in a DASH mpd 1457 1458 1459 this.manifest.contentProtection = this.manifest.contentProtection || {}; 1460 this.manifest.contentProtection['com.widevine.alpha'] = { 1461 attributes: { 1462 schemeIdUri: entry.attributes.KEYFORMAT, 1463 // remove '0x' from the key id string 1464 keyId: entry.attributes.KEYID.substring(2) 1465 }, 1466 // decode the base64-encoded PSSH box 1467 pssh: decodeB64ToUint8Array(entry.attributes.URI.split(',')[1]) 1468 }; 1469 return; 1470 } 1471 1472 if (!entry.attributes.METHOD) { 1473 this.trigger('warn', { 1474 message: 'defaulting key method to AES-128' 1475 }); 1476 } // setup an encryption key for upcoming segments 1477 1478 1479 key = { 1480 method: entry.attributes.METHOD || 'AES-128', 1481 uri: entry.attributes.URI 1482 }; 1483 1484 if (typeof entry.attributes.IV !== 'undefined') { 1485 key.iv = entry.attributes.IV; 1486 } 1487 }, 1488 1489 'media-sequence'() { 1490 if (!isFinite(entry.number)) { 1491 this.trigger('warn', { 1492 message: 'ignoring invalid media sequence: ' + entry.number 1493 }); 1494 return; 1495 } 1496 1497 this.manifest.mediaSequence = entry.number; 1498 }, 1499 1500 'discontinuity-sequence'() { 1501 if (!isFinite(entry.number)) { 1502 this.trigger('warn', { 1503 message: 'ignoring invalid discontinuity sequence: ' + entry.number 1504 }); 1505 return; 1506 } 1507 1508 this.manifest.discontinuitySequence = entry.number; 1509 currentTimeline = entry.number; 1510 }, 1511 1512 'playlist-type'() { 1513 if (!/VOD|EVENT/.test(entry.playlistType)) { 1514 this.trigger('warn', { 1515 message: 'ignoring unknown playlist type: ' + entry.playlist 1516 }); 1517 return; 1518 } 1519 1520 this.manifest.playlistType = entry.playlistType; 1521 }, 1522 1523 map() { 1524 currentMap = {}; 1525 1526 if (entry.uri) { 1527 currentMap.uri = entry.uri; 1528 } 1529 1530 if (entry.byterange) { 1531 currentMap.byterange = entry.byterange; 1532 } 1533 1534 if (key) { 1535 currentMap.key = key; 1536 } 1537 }, 1538 1539 'stream-inf'() { 1540 this.manifest.playlists = uris; 1541 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 1542 1543 if (!entry.attributes) { 1544 this.trigger('warn', { 1545 message: 'ignoring empty stream-inf attributes' 1546 }); 1547 return; 1548 } 1549 1550 if (!currentUri.attributes) { 1551 currentUri.attributes = {}; 1552 } 1553 1554 _extends(currentUri.attributes, entry.attributes); 1555 }, 1556 1557 media() { 1558 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 1559 1560 if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { 1561 this.trigger('warn', { 1562 message: 'ignoring incomplete or missing media group' 1563 }); 1564 return; 1565 } // find the media group, creating defaults as necessary 1566 1567 1568 const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; 1569 mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; 1570 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata 1571 1572 rendition = { 1573 default: /yes/i.test(entry.attributes.DEFAULT) 1574 }; 1575 1576 if (rendition.default) { 1577 rendition.autoselect = true; 1578 } else { 1579 rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); 1580 } 1581 1582 if (entry.attributes.LANGUAGE) { 1583 rendition.language = entry.attributes.LANGUAGE; 1584 } 1585 1586 if (entry.attributes.URI) { 1587 rendition.uri = entry.attributes.URI; 1588 } 1589 1590 if (entry.attributes['INSTREAM-ID']) { 1591 rendition.instreamId = entry.attributes['INSTREAM-ID']; 1592 } 1593 1594 if (entry.attributes.CHARACTERISTICS) { 1595 rendition.characteristics = entry.attributes.CHARACTERISTICS; 1596 } 1597 1598 if (entry.attributes.FORCED) { 1599 rendition.forced = /yes/i.test(entry.attributes.FORCED); 1600 } // insert the new rendition 1601 1602 1603 mediaGroup[entry.attributes.NAME] = rendition; 1604 }, 1605 1606 discontinuity() { 1607 currentTimeline += 1; 1608 currentUri.discontinuity = true; 1609 this.manifest.discontinuityStarts.push(uris.length); 1610 }, 1611 1612 'program-date-time'() { 1613 if (typeof this.manifest.dateTimeString === 'undefined') { 1614 // PROGRAM-DATE-TIME is a media-segment tag, but for backwards 1615 // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag 1616 // to the manifest object 1617 // TODO: Consider removing this in future major version 1618 this.manifest.dateTimeString = entry.dateTimeString; 1619 this.manifest.dateTimeObject = entry.dateTimeObject; 1620 } 1621 1622 currentUri.dateTimeString = entry.dateTimeString; 1623 currentUri.dateTimeObject = entry.dateTimeObject; 1624 const { 1625 lastProgramDateTime 1626 } = this; 1627 this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence. 1628 // Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference. 1629 1630 if (lastProgramDateTime === null) { 1631 // Extrapolate Program Date Time backward 1632 // Since it is first program date time occurrence we're assuming that 1633 // all this.manifest.segments have no program date time info 1634 this.manifest.segments.reduceRight((programDateTime, segment) => { 1635 segment.programDateTime = programDateTime - segment.duration * 1000; 1636 return segment.programDateTime; 1637 }, this.lastProgramDateTime); 1638 } 1639 }, 1640 1641 targetduration() { 1642 if (!isFinite(entry.duration) || entry.duration < 0) { 1643 this.trigger('warn', { 1644 message: 'ignoring invalid target duration: ' + entry.duration 1645 }); 1646 return; 1647 } 1648 1649 this.manifest.targetDuration = entry.duration; 1650 setHoldBack.call(this, this.manifest); 1651 }, 1652 1653 start() { 1654 if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { 1655 this.trigger('warn', { 1656 message: 'ignoring start declaration without appropriate attribute list' 1657 }); 1658 return; 1659 } 1660 1661 this.manifest.start = { 1662 timeOffset: entry.attributes['TIME-OFFSET'], 1663 precise: entry.attributes.PRECISE 1664 }; 1665 }, 1666 1667 'cue-out'() { 1668 currentUri.cueOut = entry.data; 1669 }, 1670 1671 'cue-out-cont'() { 1672 currentUri.cueOutCont = entry.data; 1673 }, 1674 1675 'cue-in'() { 1676 currentUri.cueIn = entry.data; 1677 }, 1678 1679 'skip'() { 1680 this.manifest.skip = camelCaseKeys(entry.attributes); 1681 this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']); 1682 }, 1683 1684 'part'() { 1685 hasParts = true; // parts are always specifed before a segment 1686 1687 const segmentIndex = this.manifest.segments.length; 1688 const part = camelCaseKeys(entry.attributes); 1689 currentUri.parts = currentUri.parts || []; 1690 currentUri.parts.push(part); 1691 1692 if (part.byterange) { 1693 if (!part.byterange.hasOwnProperty('offset')) { 1694 part.byterange.offset = lastPartByterangeEnd; 1695 } 1696 1697 lastPartByterangeEnd = part.byterange.offset + part.byterange.length; 1698 } 1699 1700 const partIndex = currentUri.parts.length - 1; 1701 this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']); 1702 1703 if (this.manifest.renditionReports) { 1704 this.manifest.renditionReports.forEach((r, i) => { 1705 if (!r.hasOwnProperty('lastPart')) { 1706 this.trigger('warn', { 1707 message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART` 1708 }); 1709 } 1710 }); 1711 } 1712 }, 1713 1714 'server-control'() { 1715 const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes); 1716 1717 if (!attrs.hasOwnProperty('canBlockReload')) { 1718 attrs.canBlockReload = false; 1719 this.trigger('info', { 1720 message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false' 1721 }); 1722 } 1723 1724 setHoldBack.call(this, this.manifest); 1725 1726 if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) { 1727 this.trigger('warn', { 1728 message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set' 1729 }); 1730 } 1731 }, 1732 1733 'preload-hint'() { 1734 // parts are always specifed before a segment 1735 const segmentIndex = this.manifest.segments.length; 1736 const hint = camelCaseKeys(entry.attributes); 1737 const isPart = hint.type && hint.type === 'PART'; 1738 currentUri.preloadHints = currentUri.preloadHints || []; 1739 currentUri.preloadHints.push(hint); 1740 1741 if (hint.byterange) { 1742 if (!hint.byterange.hasOwnProperty('offset')) { 1743 // use last part byterange end or zero if not a part. 1744 hint.byterange.offset = isPart ? lastPartByterangeEnd : 0; 1745 1746 if (isPart) { 1747 lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length; 1748 } 1749 } 1750 } 1751 1752 const index = currentUri.preloadHints.length - 1; 1753 this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']); 1754 1755 if (!hint.type) { 1756 return; 1757 } // search through all preload hints except for the current one for 1758 // a duplicate type. 1759 1760 1761 for (let i = 0; i < currentUri.preloadHints.length - 1; i++) { 1762 const otherHint = currentUri.preloadHints[i]; 1763 1764 if (!otherHint.type) { 1765 continue; 1766 } 1767 1768 if (otherHint.type === hint.type) { 1769 this.trigger('warn', { 1770 message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}` 1771 }); 1772 } 1773 } 1774 }, 1775 1776 'rendition-report'() { 1777 const report = camelCaseKeys(entry.attributes); 1778 this.manifest.renditionReports = this.manifest.renditionReports || []; 1779 this.manifest.renditionReports.push(report); 1780 const index = this.manifest.renditionReports.length - 1; 1781 const required = ['LAST-MSN', 'URI']; 1782 1783 if (hasParts) { 1784 required.push('LAST-PART'); 1785 } 1786 1787 this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required); 1788 }, 1789 1790 'part-inf'() { 1791 this.manifest.partInf = camelCaseKeys(entry.attributes); 1792 this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']); 1793 1794 if (this.manifest.partInf.partTarget) { 1795 this.manifest.partTargetDuration = this.manifest.partInf.partTarget; 1796 } 1797 1798 setHoldBack.call(this, this.manifest); 1799 }, 1800 1801 'daterange'() { 1802 this.manifest.dateRanges.push(camelCaseKeys(entry.attributes)); 1803 const index = this.manifest.dateRanges.length - 1; 1804 this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']); 1805 const dateRange = this.manifest.dateRanges[index]; 1806 1807 if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) { 1808 this.trigger('warn', { 1809 message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE' 1810 }); 1811 } 1812 1813 if (dateRange.duration && dateRange.duration < 0) { 1814 this.trigger('warn', { 1815 message: 'EXT-X-DATERANGE DURATION must not be negative' 1816 }); 1817 } 1818 1819 if (dateRange.plannedDuration && dateRange.plannedDuration < 0) { 1820 this.trigger('warn', { 1821 message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative' 1822 }); 1823 } 1824 1825 const endOnNextYes = !!dateRange.endOnNext; 1826 1827 if (endOnNextYes && !dateRange.class) { 1828 this.trigger('warn', { 1829 message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute' 1830 }); 1831 } 1832 1833 if (endOnNextYes && (dateRange.duration || dateRange.endDate)) { 1834 this.trigger('warn', { 1835 message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes' 1836 }); 1837 } 1838 1839 if (dateRange.duration && dateRange.endDate) { 1840 const startDate = dateRange.startDate; 1841 const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000; 1842 this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds); 1843 } 1844 1845 if (!dateRangeTags[dateRange.id]) { 1846 dateRangeTags[dateRange.id] = dateRange; 1847 } else { 1848 for (const attribute in dateRangeTags[dateRange.id]) { 1849 if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) { 1850 this.trigger('warn', { 1851 message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values' 1852 }); 1853 break; 1854 } 1855 } // if tags with the same ID do not have conflicting attributes, merge them 1856 1857 1858 const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id); 1859 this.manifest.dateRanges[dateRangeWithSameId] = _extends(this.manifest.dateRanges[dateRangeWithSameId], dateRange); 1860 dateRangeTags[dateRange.id] = _extends(dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last 1861 1862 this.manifest.dateRanges.pop(); 1863 } 1864 }, 1865 1866 'independent-segments'() { 1867 this.manifest.independentSegments = true; 1868 }, 1869 1870 'i-frames-only'() { 1871 this.manifest.iFramesOnly = true; 1872 this.requiredCompatibilityversion(this.manifest.version, 4); 1873 }, 1874 1875 'content-steering'() { 1876 this.manifest.contentSteering = camelCaseKeys(entry.attributes); 1877 this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']); 1878 }, 1879 1880 /** @this {Parser} */ 1881 define() { 1882 this.manifest.definitions = this.manifest.definitions || {}; 1883 1884 const addDef = (n, v) => { 1885 if (n in this.manifest.definitions) { 1886 // An EXT-X-DEFINE tag MUST NOT specify the same Variable Name as any other 1887 // EXT-X-DEFINE tag in the same Playlist. Parsers that encounter duplicate 1888 // Variable Name declarations MUST fail to parse the Playlist. 1889 this.trigger('error', { 1890 message: `EXT-X-DEFINE: Duplicate name ${n}` 1891 }); 1892 return; 1893 } 1894 1895 this.manifest.definitions[n] = v; 1896 }; 1897 1898 if ('QUERYPARAM' in entry.attributes) { 1899 if ('NAME' in entry.attributes || 'IMPORT' in entry.attributes) { 1900 // An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a 1901 // QUERYPARAM attribute, but only one of the three. Otherwise, the 1902 // client MUST fail to parse the Playlist. 1903 this.trigger('error', { 1904 message: 'EXT-X-DEFINE: Invalid attributes' 1905 }); 1906 return; 1907 } 1908 1909 const val = this.params.get(entry.attributes.QUERYPARAM); 1910 1911 if (!val) { 1912 // If the QUERYPARAM attribute value does not match any query parameter in 1913 // the URI or the matching parameter has no associated value, the parser 1914 // MUST fail to parse the Playlist. If more than one parameter matches, 1915 // any of the associated values MAY be used. 1916 this.trigger('error', { 1917 message: `EXT-X-DEFINE: No query param ${entry.attributes.QUERYPARAM}` 1918 }); 1919 return; 1920 } 1921 1922 addDef(entry.attributes.QUERYPARAM, decodeURIComponent(val)); 1923 return; 1924 } 1925 1926 if ('NAME' in entry.attributes) { 1927 if ('IMPORT' in entry.attributes) { 1928 // An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a 1929 // QUERYPARAM attribute, but only one of the three. Otherwise, the 1930 // client MUST fail to parse the Playlist. 1931 this.trigger('error', { 1932 message: 'EXT-X-DEFINE: Invalid attributes' 1933 }); 1934 return; 1935 } 1936 1937 if (!('VALUE' in entry.attributes) || typeof entry.attributes.VALUE !== 'string') { 1938 // This attribute is REQUIRED if the EXT-X-DEFINE tag has a NAME attribute. 1939 // The quoted-string MAY be empty. 1940 this.trigger('error', { 1941 message: `EXT-X-DEFINE: No value for ${entry.attributes.NAME}` 1942 }); 1943 return; 1944 } 1945 1946 addDef(entry.attributes.NAME, entry.attributes.VALUE); 1947 return; 1948 } 1949 1950 if ('IMPORT' in entry.attributes) { 1951 if (!this.mainDefinitions[entry.attributes.IMPORT]) { 1952 // Covers two conditions, as mainDefinitions will always be empty on main 1953 // 1954 // EXT-X-DEFINE tags containing the IMPORT attribute MUST NOT occur in 1955 // Multivariant Playlists; they are only allowed in Media Playlists. 1956 // 1957 // If the IMPORT attribute value does not match any Variable Name in the 1958 // Multivariant Playlist, or if the Media Playlist loaded from a 1959 // Multivariant Playlist, the parser MUST fail the Playlist. 1960 this.trigger('error', { 1961 message: `EXT-X-DEFINE: No value ${entry.attributes.IMPORT} to import, or IMPORT used on main playlist` 1962 }); 1963 return; 1964 } 1965 1966 addDef(entry.attributes.IMPORT, this.mainDefinitions[entry.attributes.IMPORT]); 1967 return; 1968 } // An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a QUERYPARAM 1969 // attribute, but only one of the three. Otherwise, the client MUST fail to 1970 // parse the Playlist. 1971 1972 1973 this.trigger('error', { 1974 message: 'EXT-X-DEFINE: No attribute' 1975 }); 1976 }, 1977 1978 'i-frame-playlist'() { 1979 this.manifest.iFramePlaylists.push({ 1980 attributes: entry.attributes, 1981 uri: entry.uri, 1982 timeline: currentTimeline 1983 }); 1984 this.warnOnMissingAttributes_('#EXT-X-I-FRAME-STREAM-INF', entry.attributes, ['BANDWIDTH', 'URI']); 1985 } 1986 1987 })[entry.tagType] || noop).call(self); 1988 }, 1989 1990 uri() { 1991 currentUri.uri = entry.uri; 1992 uris.push(currentUri); // if no explicit duration was declared, use the target duration 1993 1994 if (this.manifest.targetDuration && !('duration' in currentUri)) { 1995 this.trigger('warn', { 1996 message: 'defaulting segment duration to the target duration' 1997 }); 1998 currentUri.duration = this.manifest.targetDuration; 1999 } // annotate with encryption information, if necessary 2000 2001 2002 if (key) { 2003 currentUri.key = key; 2004 } 2005 2006 currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary 2007 2008 if (currentMap) { 2009 currentUri.map = currentMap; 2010 } // reset the last byterange end as it needs to be 0 between parts 2011 2012 2013 lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward 2014 2015 if (this.lastProgramDateTime !== null) { 2016 currentUri.programDateTime = this.lastProgramDateTime; 2017 this.lastProgramDateTime += currentUri.duration * 1000; 2018 } // prepare for the next URI 2019 2020 2021 currentUri = {}; 2022 }, 2023 2024 comment() {// comments are not important for playback 2025 }, 2026 2027 custom() { 2028 // if this is segment-level data attach the output to the segment 2029 if (entry.segment) { 2030 currentUri.custom = currentUri.custom || {}; 2031 currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object 2032 } else { 2033 this.manifest.custom = this.manifest.custom || {}; 2034 this.manifest.custom[entry.customType] = entry.data; 2035 } 2036 } 2037 2038 })[entry.type].call(self); 2039 }); 2040 } 2041 2042 requiredCompatibilityversion(currentVersion, targetVersion) { 2043 if (currentVersion < targetVersion || !currentVersion) { 2044 this.trigger('warn', { 2045 message: `manifest must be at least version ${targetVersion}` 2046 }); 2047 } 2048 } 2049 2050 warnOnMissingAttributes_(identifier, attributes, required) { 2051 const missing = []; 2052 required.forEach(function (key) { 2053 if (!attributes.hasOwnProperty(key)) { 2054 missing.push(key); 2055 } 2056 }); 2057 2058 if (missing.length) { 2059 this.trigger('warn', { 2060 message: `${identifier} lacks required attribute(s): ${missing.join(', ')}` 2061 }); 2062 } 2063 } 2064 /** 2065 * Parse the input string and update the manifest object. 2066 * 2067 * @param {string} chunk a potentially incomplete portion of the manifest 2068 */ 2069 2070 2071 push(chunk) { 2072 this.lineStream.push(chunk); 2073 } 2074 /** 2075 * Flush any remaining input. This can be handy if the last line of an M3U8 2076 * manifest did not contain a trailing newline but the file has been 2077 * completely received. 2078 */ 2079 2080 2081 end() { 2082 // flush any buffered input 2083 this.lineStream.push('\n'); 2084 2085 if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) { 2086 this.trigger('warn', { 2087 message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag' 2088 }); 2089 } 2090 2091 this.lastProgramDateTime = null; 2092 this.trigger('end'); 2093 } 2094 /** 2095 * Add an additional parser for non-standard tags 2096 * 2097 * @param {Object} options a map of options for the added parser 2098 * @param {RegExp} options.expression a regular expression to match the custom header 2099 * @param {string} options.customType the custom type to register to the output 2100 * @param {Function} [options.dataParser] function to parse the line into an object 2101 * @param {boolean} [options.segment] should tag data be attached to the segment object 2102 */ 2103 2104 2105 addParser(options) { 2106 this.parseStream.addParser(options); 2107 } 2108 /** 2109 * Add a custom header mapper 2110 * 2111 * @param {Object} options 2112 * @param {RegExp} options.expression a regular expression to match the custom header 2113 * @param {Function} options.map function to translate tag into a different tag 2114 */ 2115 2116 2117 addTagMapper(options) { 2118 this.parseStream.addTagMapper(options); 2119 } 2120 2121 } 2122 2123 var regexs = { 2124 // to determine mime types 2125 mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v|stpp.ttml.im1t)/, 2126 webm: /^(vp0?[89]|av0?1|opus|vorbis)/, 2127 ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/, 2128 // to determine if a codec is audio or video 2129 video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/, 2130 audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3|speex|aac)/, 2131 text: /^(stpp.ttml.im1t)/, 2132 // mux.js support regex 2133 muxerVideo: /^(avc0?1)/, 2134 muxerAudio: /^(mp4a)/, 2135 // match nothing as muxer does not support text right now. 2136 // there cannot never be a character before the start of a string 2137 // so this matches nothing. 2138 muxerText: /a^/ 2139 }; 2140 var mediaTypes = ['video', 'audio', 'text']; 2141 var upperMediaTypes = ['Video', 'Audio', 'Text']; 2142 /** 2143 * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard 2144 * `avc1.<hhhhhh>` 2145 * 2146 * @param {string} codec 2147 * Codec string to translate 2148 * @return {string} 2149 * The translated codec string 2150 */ 2151 2152 var translateLegacyCodec = function translateLegacyCodec(codec) { 2153 if (!codec) { 2154 return codec; 2155 } 2156 2157 return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { 2158 var profileHex = ('00' + Number(profile).toString(16)).slice(-2); 2159 var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); 2160 return 'avc1.' + profileHex + '00' + avcLevelHex; 2161 }); 2162 }; 2163 /** 2164 * @typedef {Object} ParsedCodecInfo 2165 * @property {number} codecCount 2166 * Number of codecs parsed 2167 * @property {string} [videoCodec] 2168 * Parsed video codec (if found) 2169 * @property {string} [videoObjectTypeIndicator] 2170 * Video object type indicator (if found) 2171 * @property {string|null} audioProfile 2172 * Audio profile 2173 */ 2174 2175 /** 2176 * Parses a codec string to retrieve the number of codecs specified, the video codec and 2177 * object type indicator, and the audio profile. 2178 * 2179 * @param {string} [codecString] 2180 * The codec string to parse 2181 * @return {ParsedCodecInfo} 2182 * Parsed codec info 2183 */ 2184 2185 var parseCodecs = function parseCodecs(codecString) { 2186 if (codecString === void 0) { 2187 codecString = ''; 2188 } 2189 2190 var codecs = codecString.split(','); 2191 var result = []; 2192 codecs.forEach(function (codec) { 2193 codec = codec.trim(); 2194 var codecType;
vendor: 134,050 bytes, lines 2195-6357
2195 mediaTypes.forEach(function (name) { 2196 var match = regexs[name].exec(codec.toLowerCase()); 2197 2198 if (!match || match.length <= 1) { 2199 return; 2200 } 2201 2202 codecType = name; // maintain codec case 2203 2204 var type = codec.substring(0, match[1].length); 2205 var details = codec.replace(type, ''); 2206 result.push({ 2207 type: type, 2208 details: details, 2209 mediaType: name 2210 }); 2211 }); 2212 2213 if (!codecType) { 2214 result.push({ 2215 type: codec, 2216 details: '', 2217 mediaType: 'unknown' 2218 }); 2219 } 2220 }); 2221 return result; 2222 }; 2223 /** 2224 * Returns a ParsedCodecInfo object for the default alternate audio playlist if there is 2225 * a default alternate audio playlist for the provided audio group. 2226 * 2227 * @param {Object} master 2228 * The master playlist 2229 * @param {string} audioGroupId 2230 * ID of the audio group for which to find the default codec info 2231 * @return {ParsedCodecInfo} 2232 * Parsed codec info 2233 */ 2234 2235 var codecsFromDefault = function codecsFromDefault(master, audioGroupId) { 2236 if (!master.mediaGroups.AUDIO || !audioGroupId) { 2237 return null; 2238 } 2239 2240 var audioGroup = master.mediaGroups.AUDIO[audioGroupId]; 2241 2242 if (!audioGroup) { 2243 return null; 2244 } 2245 2246 for (var name in audioGroup) { 2247 var audioType = audioGroup[name]; 2248 2249 if (audioType.default && audioType.playlists) { 2250 // codec should be the same for all playlists within the audio type 2251 return parseCodecs(audioType.playlists[0].attributes.CODECS); 2252 } 2253 } 2254 2255 return null; 2256 }; 2257 var isAudioCodec = function isAudioCodec(codec) { 2258 if (codec === void 0) { 2259 codec = ''; 2260 } 2261 2262 return regexs.audio.test(codec.trim().toLowerCase()); 2263 }; 2264 var isTextCodec = function isTextCodec(codec) { 2265 if (codec === void 0) { 2266 codec = ''; 2267 } 2268 2269 return regexs.text.test(codec.trim().toLowerCase()); 2270 }; 2271 var getMimeForCodec = function getMimeForCodec(codecString) { 2272 if (!codecString || typeof codecString !== 'string') { 2273 return; 2274 } 2275 2276 var codecs = codecString.toLowerCase().split(',').map(function (c) { 2277 return translateLegacyCodec(c.trim()); 2278 }); // default to video type 2279 2280 var type = 'video'; // only change to audio type if the only codec we have is 2281 // audio 2282 2283 if (codecs.length === 1 && isAudioCodec(codecs[0])) { 2284 type = 'audio'; 2285 } else if (codecs.length === 1 && isTextCodec(codecs[0])) { 2286 // text uses application/<container> for now 2287 type = 'application'; 2288 } // default the container to mp4 2289 2290 2291 var container = 'mp4'; // every codec must be able to go into the container 2292 // for that container to be the correct one 2293 2294 if (codecs.every(function (c) { 2295 return regexs.mp4.test(c); 2296 })) { 2297 container = 'mp4'; 2298 } else if (codecs.every(function (c) { 2299 return regexs.webm.test(c); 2300 })) { 2301 container = 'webm'; 2302 } else if (codecs.every(function (c) { 2303 return regexs.ogg.test(c); 2304 })) { 2305 container = 'ogg'; 2306 } 2307 2308 return type + "/" + container + ";codecs=\"" + codecString + "\""; 2309 }; 2310 /** 2311 * Tests whether the codec is supported by MediaSource. Optionally also tests ManagedMediaSource. 2312 * 2313 * @param {string} codecString 2314 * Codec to test 2315 * @param {boolean} [withMMS] 2316 * Whether to check if ManagedMediaSource supports it 2317 * @return {boolean} 2318 * Codec is supported 2319 */ 2320 2321 var browserSupportsCodec = function browserSupportsCodec(codecString, withMMS) { 2322 if (codecString === void 0) { 2323 codecString = ''; 2324 } 2325 2326 if (withMMS === void 0) { 2327 withMMS = false; 2328 } 2329 2330 return window.MediaSource && window.MediaSource.isTypeSupported && window.MediaSource.isTypeSupported(getMimeForCodec(codecString)) || withMMS && window.ManagedMediaSource && window.ManagedMediaSource.isTypeSupported && window.ManagedMediaSource.isTypeSupported(getMimeForCodec(codecString)) || false; 2331 }; 2332 var muxerSupportsCodec = function muxerSupportsCodec(codecString) { 2333 if (codecString === void 0) { 2334 codecString = ''; 2335 } 2336 2337 return codecString.toLowerCase().split(',').every(function (codec) { 2338 codec = codec.trim(); // any match is supported. 2339 2340 for (var i = 0; i < upperMediaTypes.length; i++) { 2341 var type = upperMediaTypes[i]; 2342 2343 if (regexs["muxer" + type].test(codec)) { 2344 return true; 2345 } 2346 } 2347 2348 return false; 2349 }); 2350 }; 2351 var DEFAULT_AUDIO_CODEC = 'mp4a.40.2'; 2352 var DEFAULT_VIDEO_CODEC = 'avc1.4d400d'; 2353 2354 /** 2355 * Provides a compatibility layer between Video.js 7 and 8 API changes for VHS. 2356 */ 2357 /** 2358 * Delegates to videojs.obj.merge (Video.js 8) or 2359 * videojs.mergeOptions (Video.js 7). 2360 */ 2361 2362 function merge$1(...args) { 2363 const context = videojs__default["default"].obj || videojs__default["default"]; 2364 const fn = context.merge || context.mergeOptions; 2365 return fn.apply(context, args); 2366 } 2367 /** 2368 * Delegates to videojs.time.createTimeRanges (Video.js 8) or 2369 * videojs.createTimeRanges (Video.js 7). 2370 */ 2371 2372 function createTimeRanges(...args) { 2373 const context = videojs__default["default"].time || videojs__default["default"]; 2374 const fn = context.createTimeRanges || context.createTimeRanges; 2375 return fn.apply(context, args); 2376 } 2377 /** 2378 * Converts provided buffered ranges to a descriptive string 2379 * 2380 * @param {TimeRanges} buffered - received buffered time ranges 2381 * 2382 * @return {string} - descriptive string 2383 */ 2384 2385 function bufferedRangesToString(buffered) { 2386 if (buffered.length === 0) { 2387 return 'Buffered Ranges are empty'; 2388 } 2389 2390 let bufferedRangesStr = 'Buffered Ranges: \n'; 2391 2392 for (let i = 0; i < buffered.length; i++) { 2393 const start = buffered.start(i); 2394 const end = buffered.end(i); 2395 bufferedRangesStr += `${start} --> ${end}. Duration (${end - start})\n`; 2396 } 2397 2398 return bufferedRangesStr; 2399 } 2400 2401 /** 2402 * ranges 2403 * 2404 * Utilities for working with TimeRanges. 2405 * 2406 */ 2407 2408 const TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range 2409 // can be misleading because of precision differences or when the current media has poorly 2410 // aligned audio and video, which can cause values to be slightly off from what you would 2411 // expect. This value is what we consider to be safe to use in such comparisons to account 2412 // for these scenarios. 2413 2414 const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 2415 2416 const filterRanges = function (timeRanges, predicate) { 2417 const results = []; 2418 let i; 2419 2420 if (timeRanges && timeRanges.length) { 2421 // Search for ranges that match the predicate 2422 for (i = 0; i < timeRanges.length; i++) { 2423 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 2424 results.push([timeRanges.start(i), timeRanges.end(i)]); 2425 } 2426 } 2427 } 2428 2429 return createTimeRanges(results); 2430 }; 2431 /** 2432 * Attempts to find the buffered TimeRange that contains the specified 2433 * time. 2434 * 2435 * @param {TimeRanges} buffered - the TimeRanges object to query 2436 * @param {number} time - the time to filter on. 2437 * @return {TimeRanges} a new TimeRanges object 2438 */ 2439 2440 2441 const findRange = function (buffered, time) { 2442 return filterRanges(buffered, function (start, end) { 2443 return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time; 2444 }); 2445 }; 2446 /** 2447 * Returns the TimeRanges that begin later than the specified time. 2448 * 2449 * @param {TimeRanges} timeRanges - the TimeRanges object to query 2450 * @param {number} time - the time to filter on. 2451 * @return {TimeRanges} a new TimeRanges object. 2452 */ 2453 2454 const findNextRange = function (timeRanges, time) { 2455 return filterRanges(timeRanges, function (start) { 2456 return start - TIME_FUDGE_FACTOR >= time; 2457 }); 2458 }; 2459 /** 2460 * Returns gaps within a list of TimeRanges 2461 * 2462 * @param {TimeRanges} buffered - the TimeRanges object 2463 * @return {TimeRanges} a TimeRanges object of gaps 2464 */ 2465 2466 const findGaps = function (buffered) { 2467 if (buffered.length < 2) { 2468 return createTimeRanges(); 2469 } 2470 2471 const ranges = []; 2472 2473 for (let i = 1; i < buffered.length; i++) { 2474 const start = buffered.end(i - 1); 2475 const end = buffered.start(i); 2476 ranges.push([start, end]); 2477 } 2478 2479 return createTimeRanges(ranges); 2480 }; 2481 /** 2482 * Calculate the intersection of two TimeRanges 2483 * 2484 * @param {TimeRanges} bufferA 2485 * @param {TimeRanges} bufferB 2486 * @return {TimeRanges} The interesection of `bufferA` with `bufferB` 2487 */ 2488 2489 const bufferIntersection = function (bufferA, bufferB) { 2490 let start = null; 2491 let end = null; 2492 let arity = 0; 2493 const extents = []; 2494 const ranges = []; 2495 2496 if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) { 2497 return createTimeRanges(); 2498 } // Handle the case where we have both buffers and create an 2499 // intersection of the two 2500 2501 2502 let count = bufferA.length; // A) Gather up all start and end times 2503 2504 while (count--) { 2505 extents.push({ 2506 time: bufferA.start(count), 2507 type: 'start' 2508 }); 2509 extents.push({ 2510 time: bufferA.end(count), 2511 type: 'end' 2512 }); 2513 } 2514 2515 count = bufferB.length; 2516 2517 while (count--) { 2518 extents.push({ 2519 time: bufferB.start(count), 2520 type: 'start' 2521 }); 2522 extents.push({ 2523 time: bufferB.end(count), 2524 type: 'end' 2525 }); 2526 } // B) Sort them by time 2527 2528 2529 extents.sort(function (a, b) { 2530 return a.time - b.time; 2531 }); // C) Go along one by one incrementing arity for start and decrementing 2532 // arity for ends 2533 2534 for (count = 0; count < extents.length; count++) { 2535 if (extents[count].type === 'start') { 2536 arity++; // D) If arity is ever incremented to 2 we are entering an 2537 // overlapping range 2538 2539 if (arity === 2) { 2540 start = extents[count].time; 2541 } 2542 } else if (extents[count].type === 'end') { 2543 arity--; // E) If arity is ever decremented to 1 we leaving an 2544 // overlapping range 2545 2546 if (arity === 1) { 2547 end = extents[count].time; 2548 } 2549 } // F) Record overlapping ranges 2550 2551 2552 if (start !== null && end !== null) { 2553 ranges.push([start, end]); 2554 start = null; 2555 end = null; 2556 } 2557 } 2558 2559 return createTimeRanges(ranges); 2560 }; 2561 /** 2562 * Gets a human readable string for a TimeRange 2563 * 2564 * @param {TimeRange} range 2565 * @return {string} a human readable string 2566 */ 2567 2568 const printableRange = range => { 2569 const strArr = []; 2570 2571 if (!range || !range.length) { 2572 return ''; 2573 } 2574 2575 for (let i = 0; i < range.length; i++) { 2576 strArr.push(range.start(i) + ' => ' + range.end(i)); 2577 } 2578 2579 return strArr.join(', '); 2580 }; 2581 /** 2582 * Calculates the amount of time left in seconds until the player hits the end of the 2583 * buffer and causes a rebuffer 2584 * 2585 * @param {TimeRange} buffered 2586 * The state of the buffer 2587 * @param {Numnber} currentTime 2588 * The current time of the player 2589 * @param {number} playbackRate 2590 * The current playback rate of the player. Defaults to 1. 2591 * @return {number} 2592 * Time until the player has to start rebuffering in seconds. 2593 * @function timeUntilRebuffer 2594 */ 2595 2596 const timeUntilRebuffer = function (buffered, currentTime, playbackRate = 1) { 2597 const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 2598 return (bufferedEnd - currentTime) / playbackRate; 2599 }; 2600 /** 2601 * Converts a TimeRanges object into an array representation 2602 * 2603 * @param {TimeRanges} timeRanges 2604 * @return {Array} 2605 */ 2606 2607 const timeRangesToArray = timeRanges => { 2608 const timeRangesList = []; 2609 2610 for (let i = 0; i < timeRanges.length; i++) { 2611 timeRangesList.push({ 2612 start: timeRanges.start(i), 2613 end: timeRanges.end(i) 2614 }); 2615 } 2616 2617 return timeRangesList; 2618 }; 2619 /** 2620 * Determines if two time range objects are different. 2621 * 2622 * @param {TimeRange} a 2623 * the first time range object to check 2624 * 2625 * @param {TimeRange} b 2626 * the second time range object to check 2627 * 2628 * @return {Boolean} 2629 * Whether the time range objects differ 2630 */ 2631 2632 const isRangeDifferent = function (a, b) { 2633 // same object 2634 if (a === b) { 2635 return false; 2636 } // one or the other is undefined 2637 2638 2639 if (!a && b || !b && a) { 2640 return true; 2641 } // length is different 2642 2643 2644 if (a.length !== b.length) { 2645 return true; 2646 } // see if any start/end pair is different 2647 2648 2649 for (let i = 0; i < a.length; i++) { 2650 if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) { 2651 return true; 2652 } 2653 } // if the length and every pair is the same 2654 // this is the same time range 2655 2656 2657 return false; 2658 }; 2659 const lastBufferedEnd = function (a) { 2660 if (!a || !a.length || !a.end) { 2661 return; 2662 } 2663 2664 return a.end(a.length - 1); 2665 }; 2666 /** 2667 * A utility function to add up the amount of time in a timeRange 2668 * after a specified startTime. 2669 * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0 2670 * would return 40 as there are 40s seconds after 0 in the timeRange 2671 * 2672 * @param {TimeRange} range 2673 * The range to check against 2674 * @param {number} startTime 2675 * The time in the time range that you should start counting from 2676 * 2677 * @return {number} 2678 * The number of seconds in the buffer passed the specified time. 2679 */ 2680 2681 const timeAheadOf = function (range, startTime) { 2682 let time = 0; 2683 2684 if (!range || !range.length) { 2685 return time; 2686 } 2687 2688 for (let i = 0; i < range.length; i++) { 2689 const start = range.start(i); 2690 const end = range.end(i); // startTime is after this range entirely 2691 2692 if (startTime > end) { 2693 continue; 2694 } // startTime is within this range 2695 2696 2697 if (startTime > start && startTime <= end) { 2698 time += end - startTime; 2699 continue; 2700 } // startTime is before this range. 2701 2702 2703 time += end - start; 2704 } 2705 2706 return time; 2707 }; 2708 2709 /** 2710 * @file playlist.js 2711 * 2712 * Playlist related utilities. 2713 */ 2714 /** 2715 * Get the duration of a segment, with special cases for 2716 * llhls segments that do not have a duration yet. 2717 * 2718 * @param {Object} playlist 2719 * the playlist that the segment belongs to. 2720 * @param {Object} segment 2721 * the segment to get a duration for. 2722 * 2723 * @return {number} 2724 * the segment duration 2725 */ 2726 2727 const segmentDurationWithParts = (playlist, segment) => { 2728 // if this isn't a preload segment 2729 // then we will have a segment duration that is accurate. 2730 if (!segment.preload) { 2731 return segment.duration; 2732 } // otherwise we have to add up parts and preload hints 2733 // to get an up to date duration. 2734 2735 2736 let result = 0; 2737 (segment.parts || []).forEach(function (p) { 2738 result += p.duration; 2739 }); // for preload hints we have to use partTargetDuration 2740 // as they won't even have a duration yet. 2741 2742 (segment.preloadHints || []).forEach(function (p) { 2743 if (p.type === 'PART') { 2744 result += playlist.partTargetDuration; 2745 } 2746 }); 2747 return result; 2748 }; 2749 /** 2750 * A function to get a combined list of parts and segments with durations 2751 * and indexes. 2752 * 2753 * @param {Playlist} playlist the playlist to get the list for. 2754 * 2755 * @return {Array} The part/segment list. 2756 */ 2757 2758 const getPartsAndSegments = playlist => (playlist.segments || []).reduce((acc, segment, si) => { 2759 if (segment.parts) { 2760 segment.parts.forEach(function (part, pi) { 2761 acc.push({ 2762 duration: part.duration, 2763 segmentIndex: si, 2764 partIndex: pi, 2765 part, 2766 segment 2767 }); 2768 }); 2769 } else { 2770 acc.push({ 2771 duration: segment.duration, 2772 segmentIndex: si, 2773 partIndex: null, 2774 segment, 2775 part: null 2776 }); 2777 } 2778 2779 return acc; 2780 }, []); 2781 const getLastParts = media => { 2782 const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1]; 2783 return lastSegment && lastSegment.parts || []; 2784 }; 2785 const getKnownPartCount = ({ 2786 preloadSegment 2787 }) => { 2788 if (!preloadSegment) { 2789 return; 2790 } 2791 2792 const { 2793 parts, 2794 preloadHints 2795 } = preloadSegment; 2796 let partCount = (preloadHints || []).reduce((count, hint) => count + (hint.type === 'PART' ? 1 : 0), 0); 2797 partCount += parts && parts.length ? parts.length : 0; 2798 return partCount; 2799 }; 2800 /** 2801 * Get the number of seconds to delay from the end of a 2802 * live playlist. 2803 * 2804 * @param {Playlist} main the main playlist 2805 * @param {Playlist} media the media playlist 2806 * @return {number} the hold back in seconds. 2807 */ 2808 2809 const liveEdgeDelay = (main, media) => { 2810 if (media.endList) { 2811 return 0; 2812 } // dash suggestedPresentationDelay trumps everything 2813 2814 2815 if (main && main.suggestedPresentationDelay) { 2816 return main.suggestedPresentationDelay; 2817 } 2818 2819 const hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first 2820 2821 if (hasParts && media.serverControl && media.serverControl.partHoldBack) { 2822 return media.serverControl.partHoldBack; 2823 } else if (hasParts && media.partTargetDuration) { 2824 return media.partTargetDuration * 3; // finally look for full segment delays 2825 } else if (media.serverControl && media.serverControl.holdBack) { 2826 return media.serverControl.holdBack; 2827 } else if (media.targetDuration) { 2828 return media.targetDuration * 3; 2829 } 2830 2831 return 0; 2832 }; 2833 /** 2834 * walk backward until we find a duration we can use 2835 * or return a failure 2836 * 2837 * @param {Playlist} playlist the playlist to walk through 2838 * @param {Number} endSequence the mediaSequence to stop walking on 2839 */ 2840 2841 const backwardDuration = function (playlist, endSequence) { 2842 let result = 0; 2843 let i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following 2844 // the interval, use it 2845 2846 let segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline 2847 // information that is earlier than endSequence 2848 2849 if (segment) { 2850 if (typeof segment.start !== 'undefined') { 2851 return { 2852 result: segment.start, 2853 precise: true 2854 }; 2855 } 2856 2857 if (typeof segment.end !== 'undefined') { 2858 return { 2859 result: segment.end - segment.duration, 2860 precise: true 2861 }; 2862 } 2863 } 2864 2865 while (i--) { 2866 segment = playlist.segments[i]; 2867 2868 if (typeof segment.end !== 'undefined') { 2869 return { 2870 result: result + segment.end, 2871 precise: true 2872 }; 2873 } 2874 2875 result += segmentDurationWithParts(playlist, segment); 2876 2877 if (typeof segment.start !== 'undefined') { 2878 return { 2879 result: result + segment.start, 2880 precise: true 2881 }; 2882 } 2883 } 2884 2885 return { 2886 result, 2887 precise: false 2888 }; 2889 }; 2890 /** 2891 * walk forward until we find a duration we can use 2892 * or return a failure 2893 * 2894 * @param {Playlist} playlist the playlist to walk through 2895 * @param {number} endSequence the mediaSequence to stop walking on 2896 */ 2897 2898 2899 const forwardDuration = function (playlist, endSequence) { 2900 let result = 0; 2901 let segment; 2902 let i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline 2903 // information 2904 2905 for (; i < playlist.segments.length; i++) { 2906 segment = playlist.segments[i]; 2907 2908 if (typeof segment.start !== 'undefined') { 2909 return { 2910 result: segment.start - result, 2911 precise: true 2912 }; 2913 } 2914 2915 result += segmentDurationWithParts(playlist, segment); 2916 2917 if (typeof segment.end !== 'undefined') { 2918 return { 2919 result: segment.end - result, 2920 precise: true 2921 }; 2922 } 2923 } // indicate we didn't find a useful duration estimate 2924 2925 2926 return { 2927 result: -1, 2928 precise: false 2929 }; 2930 }; 2931 /** 2932 * Calculate the media duration from the segments associated with a 2933 * playlist. The duration of a subinterval of the available segments 2934 * may be calculated by specifying an end index. 2935 * 2936 * @param {Object} playlist a media playlist object 2937 * @param {number=} endSequence an exclusive upper boundary 2938 * for the playlist. Defaults to playlist length. 2939 * @param {number} expired the amount of time that has dropped 2940 * off the front of the playlist in a live scenario 2941 * @return {number} the duration between the first available segment 2942 * and end index. 2943 */ 2944 2945 2946 const intervalDuration = function (playlist, endSequence, expired) { 2947 if (typeof endSequence === 'undefined') { 2948 endSequence = playlist.mediaSequence + playlist.segments.length; 2949 } 2950 2951 if (endSequence < playlist.mediaSequence) { 2952 return 0; 2953 } // do a backward walk to estimate the duration 2954 2955 2956 const backward = backwardDuration(playlist, endSequence); 2957 2958 if (backward.precise) { 2959 // if we were able to base our duration estimate on timing 2960 // information provided directly from the Media Source, return 2961 // it 2962 return backward.result; 2963 } // walk forward to see if a precise duration estimate can be made 2964 // that way 2965 2966 2967 const forward = forwardDuration(playlist, endSequence); 2968 2969 if (forward.precise) { 2970 // we found a segment that has been buffered and so it's 2971 // position is known precisely 2972 return forward.result; 2973 } // return the less-precise, playlist-based duration estimate 2974 2975 2976 return backward.result + expired; 2977 }; 2978 /** 2979 * Calculates the duration of a playlist. If a start and end index 2980 * are specified, the duration will be for the subset of the media 2981 * timeline between those two indices. The total duration for live 2982 * playlists is always Infinity. 2983 * 2984 * @param {Object} playlist a media playlist object 2985 * @param {number=} endSequence an exclusive upper 2986 * boundary for the playlist. Defaults to the playlist media 2987 * sequence number plus its length. 2988 * @param {number=} expired the amount of time that has 2989 * dropped off the front of the playlist in a live scenario 2990 * @return {number} the duration between the start index and end 2991 * index. 2992 */ 2993 2994 2995 const duration = function (playlist, endSequence, expired) { 2996 if (!playlist) { 2997 return 0; 2998 } 2999 3000 if (typeof expired !== 'number') { 3001 expired = 0; 3002 } // if a slice of the total duration is not requested, use 3003 // playlist-level duration indicators when they're present 3004 3005 3006 if (typeof endSequence === 'undefined') { 3007 // if present, use the duration specified in the playlist 3008 if (playlist.totalDuration) { 3009 return playlist.totalDuration; 3010 } // duration should be Infinity for live playlists 3011 3012 3013 if (!playlist.endList) { 3014 return window.Infinity; 3015 } 3016 } // calculate the total duration based on the segment durations 3017 3018 3019 return intervalDuration(playlist, endSequence, expired); 3020 }; 3021 /** 3022 * Calculate the time between two indexes in the current playlist 3023 * neight the start- nor the end-index need to be within the current 3024 * playlist in which case, the targetDuration of the playlist is used 3025 * to approximate the durations of the segments 3026 * 3027 * @param {Array} options.durationList list to iterate over for durations. 3028 * @param {number} options.defaultDuration duration to use for elements before or after the durationList 3029 * @param {number} options.startIndex partsAndSegments index to start 3030 * @param {number} options.endIndex partsAndSegments index to end. 3031 * @return {number} the number of seconds between startIndex and endIndex 3032 */ 3033 3034 const sumDurations = function ({ 3035 defaultDuration, 3036 durationList, 3037 startIndex, 3038 endIndex 3039 }) { 3040 let durations = 0; 3041 3042 if (startIndex > endIndex) { 3043 [startIndex, endIndex] = [endIndex, startIndex]; 3044 } 3045 3046 if (startIndex < 0) { 3047 for (let i = startIndex; i < Math.min(0, endIndex); i++) { 3048 durations += defaultDuration; 3049 } 3050 3051 startIndex = 0; 3052 } 3053 3054 for (let i = startIndex; i < endIndex; i++) { 3055 durations += durationList[i].duration; 3056 } 3057 3058 return durations; 3059 }; 3060 /** 3061 * Calculates the playlist end time 3062 * 3063 * @param {Object} playlist a media playlist object 3064 * @param {number=} expired the amount of time that has 3065 * dropped off the front of the playlist in a live scenario 3066 * @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 3067 * playlist end calculation should consider the safe live end 3068 * (truncate the playlist end by three segments). This is normally 3069 * used for calculating the end of the playlist's seekable range. 3070 * This takes into account the value of liveEdgePadding. 3071 * Setting liveEdgePadding to 0 is equivalent to setting this to false. 3072 * @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds. 3073 * If this is provided, it is used in the safe live end calculation. 3074 * Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent. 3075 * Corresponds to suggestedPresentationDelay in DASH manifests. 3076 * @return {number} the end time of playlist 3077 * @function playlistEnd 3078 */ 3079 3080 const playlistEnd = function (playlist, expired, useSafeLiveEnd, liveEdgePadding) { 3081 if (!playlist || !playlist.segments) { 3082 return null; 3083 } 3084 3085 if (playlist.endList) { 3086 return duration(playlist); 3087 } 3088 3089 if (expired === null) { 3090 return null; 3091 } 3092 3093 expired = expired || 0; 3094 let lastSegmentEndTime = intervalDuration(playlist, playlist.mediaSequence + playlist.segments.length, expired); 3095 3096 if (useSafeLiveEnd) { 3097 liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist); 3098 lastSegmentEndTime -= liveEdgePadding; 3099 } // don't return a time less than zero 3100 3101 3102 return Math.max(0, lastSegmentEndTime); 3103 }; 3104 /** 3105 * Calculates the interval of time that is currently seekable in a 3106 * playlist. The returned time ranges are relative to the earliest 3107 * moment in the specified playlist that is still available. A full 3108 * seekable implementation for live streams would need to offset 3109 * these values by the duration of content that has expired from the 3110 * stream. 3111 * 3112 * @param {Object} playlist a media playlist object 3113 * dropped off the front of the playlist in a live scenario 3114 * @param {number=} expired the amount of time that has 3115 * dropped off the front of the playlist in a live scenario 3116 * @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds. 3117 * Corresponds to suggestedPresentationDelay in DASH manifests. 3118 * @return {TimeRanges} the periods of time that are valid targets 3119 * for seeking 3120 */ 3121 3122 const seekable = function (playlist, expired, liveEdgePadding) { 3123 const useSafeLiveEnd = true; 3124 const seekableStart = expired || 0; 3125 let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding); 3126 3127 if (seekableEnd === null) { 3128 return createTimeRanges(); 3129 } // Clamp seekable end since it can not be less than the seekable start 3130 3131 3132 if (seekableEnd < seekableStart) { 3133 seekableEnd = seekableStart; 3134 } 3135 3136 return createTimeRanges(seekableStart, seekableEnd); 3137 }; 3138 /** 3139 * Determine the index and estimated starting time of the segment that 3140 * contains a specified playback position in a media playlist. 3141 * 3142 * @param {Object} options.playlist the media playlist to query 3143 * @param {number} options.currentTime The number of seconds since the earliest 3144 * possible position to determine the containing segment for 3145 * @param {number} options.startTime the time when the segment/part starts 3146 * @param {number} options.startingSegmentIndex the segment index to start looking at. 3147 * @param {number?} [options.startingPartIndex] the part index to look at within the segment. 3148 * 3149 * @return {Object} an object with partIndex, segmentIndex, and startTime. 3150 */ 3151 3152 const getMediaInfoForTime = function ({ 3153 playlist, 3154 currentTime, 3155 startingSegmentIndex, 3156 startingPartIndex, 3157 startTime, 3158 exactManifestTimings 3159 }) { 3160 let time = currentTime - startTime; 3161 const partsAndSegments = getPartsAndSegments(playlist); 3162 let startIndex = 0; 3163 3164 for (let i = 0; i < partsAndSegments.length; i++) { 3165 const partAndSegment = partsAndSegments[i]; 3166 3167 if (startingSegmentIndex !== partAndSegment.segmentIndex) { 3168 continue; 3169 } // skip this if part index does not match. 3170 3171 3172 if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) { 3173 continue; 3174 } 3175 3176 startIndex = i; 3177 break; 3178 } 3179 3180 if (time < 0) { 3181 // Walk backward from startIndex in the playlist, adding durations 3182 // until we find a segment that contains `time` and return it 3183 if (startIndex > 0) { 3184 for (let i = startIndex - 1; i >= 0; i--) { 3185 const partAndSegment = partsAndSegments[i]; 3186 time += partAndSegment.duration; 3187 3188 if (exactManifestTimings) { 3189 if (time < 0) { 3190 continue; 3191 } 3192 } else if (time + TIME_FUDGE_FACTOR <= 0) { 3193 continue; 3194 } 3195 3196 return { 3197 partIndex: partAndSegment.partIndex, 3198 segmentIndex: partAndSegment.segmentIndex, 3199 startTime: startTime - sumDurations({ 3200 defaultDuration: playlist.targetDuration, 3201 durationList: partsAndSegments, 3202 startIndex, 3203 endIndex: i 3204 }) 3205 }; 3206 } 3207 } // We were unable to find a good segment within the playlist 3208 // so select the first segment 3209 3210 3211 return { 3212 partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, 3213 segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, 3214 startTime: currentTime 3215 }; 3216 } // When startIndex is negative, we first walk forward to first segment 3217 // adding target durations. If we "run out of time" before getting to 3218 // the first segment, return the first segment 3219 3220 3221 if (startIndex < 0) { 3222 for (let i = startIndex; i < 0; i++) { 3223 time -= playlist.targetDuration; 3224 3225 if (time < 0) { 3226 return { 3227 partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, 3228 segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, 3229 startTime: currentTime 3230 }; 3231 } 3232 } 3233 3234 startIndex = 0; 3235 } // Walk forward from startIndex in the playlist, subtracting durations 3236 // until we find a segment that contains `time` and return it 3237 3238 3239 for (let i = startIndex; i < partsAndSegments.length; i++) { 3240 const partAndSegment = partsAndSegments[i]; 3241 time -= partAndSegment.duration; 3242 const canUseFudgeFactor = partAndSegment.duration > TIME_FUDGE_FACTOR; 3243 const isExactlyAtTheEnd = time === 0; 3244 const isExtremelyCloseToTheEnd = canUseFudgeFactor && time + TIME_FUDGE_FACTOR >= 0; 3245 3246 if (isExactlyAtTheEnd || isExtremelyCloseToTheEnd) { 3247 // 1) We are exactly at the end of the current segment. 3248 // 2) We are extremely close to the end of the current segment (The difference is less than 1 / 30). 3249 // We may encounter this situation when 3250 // we don't have exact match between segment duration info in the manifest and the actual duration of the segment 3251 // For example: 3252 // We appended 3 segments 10 seconds each, meaning we should have 30 sec buffered, 3253 // but we the actual buffered is 29.99999 3254 // 3255 // In both cases: 3256 // if we passed current time -> it means that we already played current segment 3257 // if we passed buffered.end -> it means that this segment is already loaded and buffered 3258 // we should select the next segment if we have one: 3259 if (i !== partsAndSegments.length - 1) { 3260 continue; 3261 } 3262 } 3263 3264 if (exactManifestTimings) { 3265 if (time > 0) { 3266 continue; 3267 } 3268 } else if (time - TIME_FUDGE_FACTOR >= 0) { 3269 continue; 3270 } 3271 3272 return { 3273 partIndex: partAndSegment.partIndex, 3274 segmentIndex: partAndSegment.segmentIndex, 3275 startTime: startTime + sumDurations({ 3276 defaultDuration: playlist.targetDuration, 3277 durationList: partsAndSegments, 3278 startIndex, 3279 endIndex: i 3280 }) 3281 }; 3282 } // We are out of possible candidates so load the last one... 3283 3284 3285 return { 3286 segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, 3287 partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, 3288 startTime: currentTime 3289 }; 3290 }; 3291 /** 3292 * Check whether the playlist is excluded or not. 3293 * 3294 * @param {Object} playlist the media playlist object 3295 * @return {boolean} whether the playlist is excluded or not 3296 * @function isExcluded 3297 */ 3298 3299 const isExcluded = function (playlist) { 3300 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 3301 }; 3302 /** 3303 * Check whether the playlist is compatible with current playback configuration or has 3304 * been excluded permanently for being incompatible. 3305 * 3306 * @param {Object} playlist the media playlist object 3307 * @return {boolean} whether the playlist is incompatible or not 3308 * @function isIncompatible 3309 */ 3310 3311 const isIncompatible = function (playlist) { 3312 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 3313 }; 3314 /** 3315 * Check whether the playlist is enabled or not. 3316 * 3317 * @param {Object} playlist the media playlist object 3318 * @return {boolean} whether the playlist is enabled or not 3319 * @function isEnabled 3320 */ 3321 3322 const isEnabled = function (playlist) { 3323 const excluded = isExcluded(playlist); 3324 return !playlist.disabled && !excluded; 3325 }; 3326 /** 3327 * Check whether the playlist has been manually disabled through the representations api. 3328 * 3329 * @param {Object} playlist the media playlist object 3330 * @return {boolean} whether the playlist is disabled manually or not 3331 * @function isDisabled 3332 */ 3333 3334 const isDisabled = function (playlist) { 3335 return playlist.disabled; 3336 }; 3337 /** 3338 * Returns whether the current playlist is an AES encrypted HLS stream 3339 * 3340 * @return {boolean} true if it's an AES encrypted HLS stream 3341 */ 3342 3343 const isAes = function (media) { 3344 for (let i = 0; i < media.segments.length; i++) { 3345 if (media.segments[i].key) { 3346 return true; 3347 } 3348 } 3349 3350 return false; 3351 }; 3352 /** 3353 * Checks if the playlist has a value for the specified attribute 3354 * 3355 * @param {string} attr 3356 * Attribute to check for 3357 * @param {Object} playlist 3358 * The media playlist object 3359 * @return {boolean} 3360 * Whether the playlist contains a value for the attribute or not 3361 * @function hasAttribute 3362 */ 3363 3364 const hasAttribute = function (attr, playlist) { 3365 return playlist.attributes && playlist.attributes[attr]; 3366 }; 3367 /** 3368 * Estimates the time required to complete a segment download from the specified playlist 3369 * 3370 * @param {number} segmentDuration 3371 * Duration of requested segment 3372 * @param {number} bandwidth 3373 * Current measured bandwidth of the player 3374 * @param {Object} playlist 3375 * The media playlist object 3376 * @param {number=} bytesReceived 3377 * Number of bytes already received for the request. Defaults to 0 3378 * @return {number|NaN} 3379 * The estimated time to request the segment. NaN if bandwidth information for 3380 * the given playlist is unavailable 3381 * @function estimateSegmentRequestTime 3382 */ 3383 3384 const estimateSegmentRequestTime = function (segmentDuration, bandwidth, playlist, bytesReceived = 0) { 3385 if (!hasAttribute('BANDWIDTH', playlist)) { 3386 return NaN; 3387 } 3388 3389 const size = segmentDuration * playlist.attributes.BANDWIDTH; 3390 return (size - bytesReceived * 8) / bandwidth; 3391 }; 3392 /* 3393 * Returns whether the current playlist is the lowest rendition 3394 * 3395 * @return {Boolean} true if on lowest rendition 3396 */ 3397 3398 const isLowestEnabledRendition = (main, media) => { 3399 if (main.playlists.length === 1) { 3400 return true; 3401 } 3402 3403 const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 3404 return main.playlists.filter(playlist => { 3405 if (!isEnabled(playlist)) { 3406 return false; 3407 } 3408 3409 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 3410 }).length === 0; 3411 }; 3412 const playlistMatch = (a, b) => { 3413 // both playlits are null 3414 // or only one playlist is non-null 3415 // no match 3416 if (!a && !b || !a && b || a && !b) { 3417 return false; 3418 } // playlist objects are the same, match 3419 3420 3421 if (a === b) { 3422 return true; 3423 } // first try to use id as it should be the most 3424 // accurate 3425 3426 3427 if (a.id && b.id && a.id === b.id) { 3428 return true; 3429 } // next try to use reslovedUri as it should be the 3430 // second most accurate. 3431 3432 3433 if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) { 3434 return true; 3435 } // finally try to use uri as it should be accurate 3436 // but might miss a few cases for relative uris 3437 3438 3439 if (a.uri && b.uri && a.uri === b.uri) { 3440 return true; 3441 } 3442 3443 return false; 3444 }; 3445 3446 const someAudioVariant = function (main, callback) { 3447 const AUDIO = main && main.mediaGroups && main.mediaGroups.AUDIO || {}; 3448 let found = false; 3449 3450 for (const groupName in AUDIO) { 3451 for (const label in AUDIO[groupName]) { 3452 found = callback(AUDIO[groupName][label]); 3453 3454 if (found) { 3455 break; 3456 } 3457 } 3458 3459 if (found) { 3460 break; 3461 } 3462 } 3463 3464 return !!found; 3465 }; 3466 3467 const isAudioOnly = main => { 3468 // we are audio only if we have no main playlists but do 3469 // have media group playlists. 3470 if (!main || !main.playlists || !main.playlists.length) { 3471 // without audio variants or playlists this 3472 // is not an audio only main. 3473 const found = someAudioVariant(main, variant => variant.playlists && variant.playlists.length || variant.uri); 3474 return found; 3475 } // if every playlist has only an audio codec it is audio only 3476 3477 3478 for (let i = 0; i < main.playlists.length; i++) { 3479 const playlist = main.playlists[i]; 3480 const CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist. 3481 3482 if (CODECS && CODECS.split(',').every(c => isAudioCodec(c))) { 3483 continue; 3484 } // playlist is in an audio group it is audio only 3485 3486 3487 const found = someAudioVariant(main, variant => playlistMatch(playlist, variant)); 3488 3489 if (found) { 3490 continue; 3491 } // if we make it here this playlist isn't audio and we 3492 // are not audio only 3493 3494 3495 return false; 3496 } // if we make it past every playlist without returning, then 3497 // this is an audio only playlist. 3498 3499 3500 return true; 3501 }; // exports 3502 3503 var Playlist = { 3504 liveEdgeDelay, 3505 duration, 3506 seekable, 3507 getMediaInfoForTime, 3508 isEnabled, 3509 isDisabled, 3510 isExcluded, 3511 isIncompatible, 3512 playlistEnd, 3513 isAes, 3514 hasAttribute, 3515 estimateSegmentRequestTime, 3516 isLowestEnabledRendition, 3517 isAudioOnly, 3518 playlistMatch, 3519 segmentDurationWithParts 3520 }; 3521 3522 const { 3523 log 3524 } = videojs__default["default"]; 3525 const createPlaylistID = (index, uri) => { 3526 return `${index}-${uri}`; 3527 }; // default function for creating a group id 3528 3529 const groupID = (type, group, label) => { 3530 return `placeholder-uri-${type}-${group}-${label}`; 3531 }; 3532 /** 3533 * Parses a given m3u8 playlist 3534 * 3535 * @param {Function} [onwarn] 3536 * a function to call when the parser triggers a warning event. 3537 * @param {Function} [oninfo] 3538 * a function to call when the parser triggers an info event. 3539 * @param {string} manifestString 3540 * The downloaded manifest string 3541 * @param {Object[]} [customTagParsers] 3542 * An array of custom tag parsers for the m3u8-parser instance 3543 * @param {Object[]} [customTagMappers] 3544 * An array of custom tag mappers for the m3u8-parser instance 3545 * @param {boolean} [llhls] 3546 * Whether to keep ll-hls features in the manifest after parsing. 3547 * @return {Object} 3548 * The manifest object 3549 */ 3550 3551 const parseManifest = ({ 3552 onwarn, 3553 oninfo, 3554 manifestString, 3555 customTagParsers = [], 3556 customTagMappers = [], 3557 llhls 3558 }) => { 3559 const parser = new Parser(); 3560 3561 if (onwarn) { 3562 parser.on('warn', onwarn); 3563 } 3564 3565 if (oninfo) { 3566 parser.on('info', oninfo); 3567 } 3568 3569 customTagParsers.forEach(customParser => parser.addParser(customParser)); 3570 customTagMappers.forEach(mapper => parser.addTagMapper(mapper)); 3571 parser.push(manifestString); 3572 parser.end(); 3573 const manifest = parser.manifest; // remove llhls features from the parsed manifest 3574 // if we don't want llhls support. 3575 3576 if (!llhls) { 3577 ['preloadSegment', 'skip', 'serverControl', 'renditionReports', 'partInf', 'partTargetDuration'].forEach(function (k) { 3578 if (manifest.hasOwnProperty(k)) { 3579 delete manifest[k]; 3580 } 3581 }); 3582 3583 if (manifest.segments) { 3584 manifest.segments.forEach(function (segment) { 3585 ['parts', 'preloadHints'].forEach(function (k) { 3586 if (segment.hasOwnProperty(k)) { 3587 delete segment[k]; 3588 } 3589 }); 3590 }); 3591 } 3592 } 3593 3594 if (!manifest.targetDuration) { 3595 let targetDuration = 10; 3596 3597 if (manifest.segments && manifest.segments.length) { 3598 targetDuration = manifest.segments.reduce((acc, s) => Math.max(acc, s.duration), 0); 3599 } 3600 3601 if (onwarn) { 3602 onwarn({ 3603 message: `manifest has no targetDuration defaulting to ${targetDuration}` 3604 }); 3605 } 3606 3607 manifest.targetDuration = targetDuration; 3608 } 3609 3610 const parts = getLastParts(manifest); 3611 3612 if (parts.length && !manifest.partTargetDuration) { 3613 const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0); 3614 3615 if (onwarn) { 3616 onwarn({ 3617 message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}` 3618 }); 3619 log.error('LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.'); 3620 } 3621 3622 manifest.partTargetDuration = partTargetDuration; 3623 } 3624 3625 return manifest; 3626 }; 3627 /** 3628 * Loops through all supported media groups in main and calls the provided 3629 * callback for each group 3630 * 3631 * @param {Object} main 3632 * The parsed main manifest object 3633 * @param {Function} callback 3634 * Callback to call for each media group 3635 */ 3636 3637 const forEachMediaGroup$1 = (main, callback) => { 3638 if (!main.mediaGroups) { 3639 return; 3640 } 3641 3642 ['AUDIO', 'SUBTITLES'].forEach(mediaType => { 3643 if (!main.mediaGroups[mediaType]) { 3644 return; 3645 } 3646 3647 for (const groupKey in main.mediaGroups[mediaType]) { 3648 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 3649 const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey]; 3650 callback(mediaProperties, mediaType, groupKey, labelKey); 3651 } 3652 } 3653 }); 3654 }; 3655 /** 3656 * Adds properties and attributes to the playlist to keep consistent functionality for 3657 * playlists throughout VHS. 3658 * 3659 * @param {Object} config 3660 * Arguments object 3661 * @param {Object} config.playlist 3662 * The media playlist 3663 * @param {string} [config.uri] 3664 * The uri to the media playlist (if media playlist is not from within a main 3665 * playlist) 3666 * @param {string} id 3667 * ID to use for the playlist 3668 */ 3669 3670 const setupMediaPlaylist = ({ 3671 playlist, 3672 uri, 3673 id 3674 }) => { 3675 playlist.id = id; 3676 playlist.playlistErrors_ = 0; 3677 3678 if (uri) { 3679 // For media playlists, m3u8-parser does not have access to a URI, as HLS media 3680 // playlists do not contain their own source URI, but one is needed for consistency in 3681 // VHS. 3682 playlist.uri = uri; 3683 } // For HLS main playlists, even though certain attributes MUST be defined, the 3684 // stream may still be played without them. 3685 // For HLS media playlists, m3u8-parser does not attach an attributes object to the 3686 // manifest. 3687 // 3688 // To avoid undefined reference errors through the project, and make the code easier 3689 // to write/read, add an empty attributes object for these cases. 3690 3691 3692 playlist.attributes = playlist.attributes || {}; 3693 }; 3694 /** 3695 * Adds ID, resolvedUri, and attributes properties to each playlist of the main, where 3696 * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to 3697 * playlist references to the playlists array. 3698 * 3699 * @param {Object} main 3700 * The main playlist 3701 */ 3702 3703 const setupMediaPlaylists = main => { 3704 let i = main.playlists.length; 3705 3706 while (i--) { 3707 const playlist = main.playlists[i]; 3708 setupMediaPlaylist({ 3709 playlist, 3710 id: createPlaylistID(i, playlist.uri) 3711 }); 3712 playlist.resolvedUri = resolveUrl(main.uri, playlist.uri); 3713 main.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility 3714 3715 main.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute, 3716 // the stream can be played without it. Although an attributes property may have been 3717 // added to the playlist to prevent undefined references, issue a warning to fix the 3718 // manifest. 3719 3720 if (!playlist.attributes.BANDWIDTH) { 3721 log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 3722 } 3723 } 3724 }; 3725 /** 3726 * Adds resolvedUri properties to each media group. 3727 * 3728 * @param {Object} main 3729 * The main playlist 3730 */ 3731 3732 const resolveMediaGroupUris = main => { 3733 forEachMediaGroup$1(main, properties => { 3734 if (properties.uri) { 3735 properties.resolvedUri = resolveUrl(main.uri, properties.uri); 3736 } 3737 }); 3738 }; 3739 /** 3740 * Creates a main playlist wrapper to insert a sole media playlist into. 3741 * 3742 * @param {Object} media 3743 * Media playlist 3744 * @param {string} uri 3745 * The media URI 3746 * 3747 * @return {Object} 3748 * main playlist 3749 */ 3750 3751 const mainForMedia = (media, uri) => { 3752 const id = createPlaylistID(0, uri); 3753 const main = { 3754 mediaGroups: { 3755 'AUDIO': {}, 3756 'VIDEO': {}, 3757 'CLOSED-CAPTIONS': {}, 3758 'SUBTITLES': {} 3759 }, 3760 uri: window.location.href, 3761 resolvedUri: window.location.href, 3762 playlists: [{ 3763 uri, 3764 id, 3765 resolvedUri: uri, 3766 // m3u8-parser does not attach an attributes property to media playlists so make 3767 // sure that the property is attached to avoid undefined reference errors 3768 attributes: {} 3769 }] 3770 }; // set up ID reference 3771 3772 main.playlists[id] = main.playlists[0]; // URI reference added for backwards compatibility 3773 3774 main.playlists[uri] = main.playlists[0]; 3775 return main; 3776 }; 3777 /** 3778 * Does an in-place update of the main manifest to add updated playlist URI references 3779 * as well as other properties needed by VHS that aren't included by the parser. 3780 * 3781 * @param {Object} main 3782 * main manifest object 3783 * @param {string} uri 3784 * The source URI 3785 * @param {function} createGroupID 3786 * A function to determine how to create the groupID for mediaGroups 3787 */ 3788 3789 const addPropertiesToMain = (main, uri, createGroupID = groupID) => { 3790 main.uri = uri; 3791 3792 for (let i = 0; i < main.playlists.length; i++) { 3793 if (!main.playlists[i].uri) { 3794 // Set up phony URIs for the playlists since playlists are referenced by their URIs 3795 // throughout VHS, but some formats (e.g., DASH) don't have external URIs 3796 // TODO: consider adding dummy URIs in mpd-parser 3797 const phonyUri = `placeholder-uri-${i}`; 3798 main.playlists[i].uri = phonyUri; 3799 } 3800 } 3801 3802 const audioOnlyMain = isAudioOnly(main); 3803 forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => { 3804 // add a playlist array under properties 3805 if (!properties.playlists || !properties.playlists.length) { 3806 // If the manifest is audio only and this media group does not have a uri, check 3807 // if the media group is located in the main list of playlists. If it is, don't add 3808 // placeholder properties as it shouldn't be considered an alternate audio track. 3809 if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) { 3810 for (let i = 0; i < main.playlists.length; i++) { 3811 const p = main.playlists[i]; 3812 3813 if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) { 3814 return; 3815 } 3816 } 3817 } 3818 3819 properties.playlists = [_extends({}, properties)]; 3820 } 3821 3822 properties.playlists.forEach(function (p, i) { 3823 const groupId = createGroupID(mediaType, groupKey, labelKey, p); 3824 const id = createPlaylistID(i, groupId); 3825 3826 if (p.uri) { 3827 p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri); 3828 } else { 3829 // DEPRECATED, this has been added to prevent a breaking change. 3830 // previously we only ever had a single media group playlist, so 3831 // we mark the first playlist uri without prepending the index as we used to 3832 // ideally we would do all of the playlists the same way. 3833 p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use 3834 // the placeholder again 3835 3836 p.resolvedUri = p.uri; 3837 } 3838 3839 p.id = p.id || id; // add an empty attributes object, all playlists are 3840 // expected to have this. 3841 3842 p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility) 3843 3844 main.playlists[p.id] = p; 3845 main.playlists[p.uri] = p; 3846 }); 3847 }); 3848 setupMediaPlaylists(main); 3849 resolveMediaGroupUris(main); 3850 }; 3851 3852 class DateRangesStorage { 3853 constructor() { 3854 this.offset_ = null; 3855 this.pendingDateRanges_ = new Map(); 3856 this.processedDateRanges_ = new Map(); 3857 } 3858 3859 setOffset(segments = []) { 3860 // already set 3861 if (this.offset_ !== null) { 3862 return; 3863 } // no segment to process 3864 3865 3866 if (!segments.length) { 3867 return; 3868 } 3869 3870 const [firstSegment] = segments; // no program date time 3871 3872 if (firstSegment.programDateTime === undefined) { 3873 return; 3874 } // Set offset as ProgramDateTime for the very first segment of the very first playlist load: 3875 3876 3877 this.offset_ = firstSegment.programDateTime / 1000; 3878 } 3879 3880 setPendingDateRanges(dateRanges = []) { 3881 if (!dateRanges.length) { 3882 return; 3883 } 3884 3885 const [dateRange] = dateRanges; 3886 const startTime = dateRange.startDate.getTime(); 3887 this.trimProcessedDateRanges_(startTime); 3888 this.pendingDateRanges_ = dateRanges.reduce((map, pendingDateRange) => { 3889 map.set(pendingDateRange.id, pendingDateRange); 3890 return map; 3891 }, new Map()); 3892 } 3893 3894 processDateRange(dateRange) { 3895 this.pendingDateRanges_.delete(dateRange.id); 3896 this.processedDateRanges_.set(dateRange.id, dateRange); 3897 } 3898 3899 getDateRangesToProcess() { 3900 if (this.offset_ === null) { 3901 return []; 3902 } 3903 3904 const dateRangeClasses = {}; 3905 const dateRangesToProcess = []; 3906 this.pendingDateRanges_.forEach((dateRange, id) => { 3907 if (this.processedDateRanges_.has(id)) { 3908 return; 3909 } 3910 3911 dateRange.startTime = dateRange.startDate.getTime() / 1000 - this.offset_; 3912 3913 dateRange.processDateRange = () => this.processDateRange(dateRange); 3914 3915 dateRangesToProcess.push(dateRange); 3916 3917 if (!dateRange.class) { 3918 return; 3919 } 3920 3921 if (dateRangeClasses[dateRange.class]) { 3922 const length = dateRangeClasses[dateRange.class].push(dateRange); 3923 dateRange.classListIndex = length - 1; 3924 } else { 3925 dateRangeClasses[dateRange.class] = [dateRange]; 3926 dateRange.classListIndex = 0; 3927 } 3928 }); 3929 3930 for (const dateRange of dateRangesToProcess) { 3931 const classList = dateRangeClasses[dateRange.class] || []; 3932 3933 if (dateRange.endDate) { 3934 dateRange.endTime = dateRange.endDate.getTime() / 1000 - this.offset_; 3935 } else if (dateRange.endOnNext && classList[dateRange.classListIndex + 1]) { 3936 dateRange.endTime = classList[dateRange.classListIndex + 1].startTime; 3937 } else if (dateRange.duration) { 3938 dateRange.endTime = dateRange.startTime + dateRange.duration; 3939 } else if (dateRange.plannedDuration) { 3940 dateRange.endTime = dateRange.startTime + dateRange.plannedDuration; 3941 } else { 3942 dateRange.endTime = dateRange.startTime; 3943 } 3944 } 3945 3946 return dateRangesToProcess; 3947 } 3948 3949 trimProcessedDateRanges_(startTime) { 3950 const copy = new Map(this.processedDateRanges_); 3951 copy.forEach((dateRange, id) => { 3952 if (dateRange.startDate.getTime() < startTime) { 3953 this.processedDateRanges_.delete(id); 3954 } 3955 }); 3956 } 3957 3958 } 3959 3960 const QUOTA_EXCEEDED_ERR = 22; 3961 const getStreamingNetworkErrorMetadata = ({ 3962 requestType, 3963 request, 3964 error, 3965 parseFailure 3966 }) => { 3967 const isBadStatus = request.status < 200 || request.status > 299; 3968 const isFailure = request.status >= 400 && request.status <= 499; 3969 const errorMetadata = { 3970 uri: request.uri, 3971 requestType 3972 }; 3973 const isBadStatusOrParseFailure = isBadStatus && !isFailure || parseFailure; 3974 3975 if (error && isFailure) { 3976 // copy original error and add to the metadata. 3977 errorMetadata.error = _extends({}, error); 3978 errorMetadata.errorType = videojs__default["default"].Error.NetworkRequestFailed; 3979 } else if (request.aborted) { 3980 errorMetadata.errorType = videojs__default["default"].Error.NetworkRequestAborted; 3981 } else if (request.timedout) { 3982 errorMetadata.errorType = videojs__default["default"].Error.NetworkRequestTimeout; 3983 } else if (isBadStatusOrParseFailure) { 3984 const errorType = parseFailure ? videojs__default["default"].Error.NetworkBodyParserFailed : videojs__default["default"].Error.NetworkBadStatus; 3985 errorMetadata.errorType = errorType; 3986 errorMetadata.status = request.status; 3987 errorMetadata.headers = request.headers; 3988 } 3989 3990 return errorMetadata; 3991 }; 3992 3993 /** 3994 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 3995 * codec strings, or translating codec strings into objects that can be examined. 3996 */ 3997 const logFn$1 = logger('CodecUtils'); 3998 /** 3999 * Returns a set of codec strings parsed from the playlist or the default 4000 * codec strings if no codecs were specified in the playlist 4001 * 4002 * @param {Playlist} media the current media playlist 4003 * @return {Object} an object with the video and audio codecs 4004 */ 4005 4006 const getCodecs = function (media) { 4007 // if the codecs were explicitly specified, use them instead of the 4008 // defaults 4009 const mediaAttributes = media.attributes || {}; 4010 4011 if (mediaAttributes.CODECS) { 4012 return parseCodecs(mediaAttributes.CODECS); 4013 } 4014 }; 4015 const isMaat = (main, media) => { 4016 const mediaAttributes = media.attributes || {}; 4017 return main && main.mediaGroups && main.mediaGroups.AUDIO && mediaAttributes.AUDIO && main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 4018 }; 4019 const isMuxed = (main, media) => { 4020 if (!isMaat(main, media)) { 4021 return true; 4022 } 4023 4024 const mediaAttributes = media.attributes || {}; 4025 const audioGroup = main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 4026 4027 for (const groupId in audioGroup) { 4028 // If an audio group has a URI (the case for HLS, as HLS will use external playlists), 4029 // or there are listed playlists (the case for DASH, as the manifest will have already 4030 // provided all of the details necessary to generate the audio playlist, as opposed to 4031 // HLS' externally requested playlists), then the content is demuxed. 4032 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 4033 return true; 4034 } 4035 } 4036 4037 return false; 4038 }; 4039 const unwrapCodecList = function (codecList) { 4040 const codecs = {}; 4041 codecList.forEach(({ 4042 mediaType, 4043 type, 4044 details 4045 }) => { 4046 codecs[mediaType] = codecs[mediaType] || []; 4047 codecs[mediaType].push(translateLegacyCodec(`${type}${details}`)); 4048 }); 4049 Object.keys(codecs).forEach(function (mediaType) { 4050 if (codecs[mediaType].length > 1) { 4051 logFn$1(`multiple ${mediaType} codecs found as attributes: ${codecs[mediaType].join(', ')}. Setting playlist codecs to null so that we wait for mux.js to probe segments for real codecs.`); 4052 codecs[mediaType] = null; 4053 return; 4054 } 4055 4056 codecs[mediaType] = codecs[mediaType][0]; 4057 }); 4058 return codecs; 4059 }; 4060 const codecCount = function (codecObj) { 4061 let count = 0; 4062 4063 if (codecObj.audio) { 4064 count++; 4065 } 4066 4067 if (codecObj.video) { 4068 count++; 4069 } 4070 4071 return count; 4072 }; 4073 /** 4074 * Calculates the codec strings for a working configuration of 4075 * SourceBuffers to play variant streams in a main playlist. If 4076 * there is no possible working configuration, an empty object will be 4077 * returned. 4078 * 4079 * @param main {Object} the m3u8 object for the main playlist 4080 * @param media {Object} the m3u8 object for the variant playlist 4081 * @return {Object} the codec strings. 4082 * 4083 * @private 4084 */ 4085 4086 const codecsForPlaylist = function (main, media) { 4087 const mediaAttributes = media.attributes || {}; 4088 const codecInfo = unwrapCodecList(getCodecs(media) || []); // HLS with multiple-audio tracks must always get an audio codec. 4089 // Put another way, there is no way to have a video-only multiple-audio HLS! 4090 4091 if (isMaat(main, media) && !codecInfo.audio) { 4092 if (!isMuxed(main, media)) { 4093 // It is possible for codecs to be specified on the audio media group playlist but 4094 // not on the rendition playlist. This is mostly the case for DASH, where audio and 4095 // video are always separate (and separately specified). 4096 const defaultCodecs = unwrapCodecList(codecsFromDefault(main, mediaAttributes.AUDIO) || []); 4097 4098 if (defaultCodecs.audio) { 4099 codecInfo.audio = defaultCodecs.audio; 4100 } 4101 } 4102 } 4103 4104 return codecInfo; 4105 }; 4106 4107 const { 4108 EventTarget: EventTarget$1 4109 } = videojs__default["default"]; 4110 4111 const addLLHLSQueryDirectives = (uri, media) => { 4112 if (media.endList || !media.serverControl) { 4113 return uri; 4114 } 4115 4116 const parameters = {}; 4117 4118 if (media.serverControl.canBlockReload) { 4119 const { 4120 preloadSegment 4121 } = media; // next msn is a zero based value, length is not. 4122 4123 let nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely 4124 // that we are going to request a part of that preload segment. 4125 // the logic below is used to determine that. 4126 4127 if (preloadSegment) { 4128 const parts = preloadSegment.parts || []; // _HLS_part is a zero based index 4129 4130 const nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the 4131 // length of parts, then we know we had part preload hints 4132 // and we need to add the _HLS_part= query 4133 4134 if (nextPart > -1 && nextPart !== parts.length - 1) { 4135 // add existing parts to our preload hints 4136 // eslint-disable-next-line 4137 parameters._HLS_part = nextPart; 4138 } // this if statement makes sure that we request the msn 4139 // of the preload segment if: 4140 // 1. the preload segment had parts (and was not yet a full segment) 4141 // but was added to our segments array 4142 // 2. the preload segment had preload hints for parts that are not in 4143 // the manifest yet. 4144 // in all other cases we want the segment after the preload segment 4145 // which will be given by using media.segments.length because it is 1 based 4146 // rather than 0 based. 4147 4148 4149 if (nextPart > -1 || parts.length) { 4150 nextMSN--; 4151 } 4152 } // add _HLS_msn= in front of any _HLS_part query 4153 // eslint-disable-next-line 4154 4155 4156 parameters._HLS_msn = nextMSN; 4157 } 4158 4159 if (media.serverControl && media.serverControl.canSkipUntil) { 4160 // add _HLS_skip= infront of all other queries. 4161 // eslint-disable-next-line 4162 parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES'; 4163 } 4164 4165 if (Object.keys(parameters).length) { 4166 const parsedUri = new window.URL(uri); 4167 ['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) { 4168 if (!parameters.hasOwnProperty(name)) { 4169 return; 4170 } 4171 4172 parsedUri.searchParams.set(name, parameters[name]); 4173 }); 4174 uri = parsedUri.toString(); 4175 } 4176 4177 return uri; 4178 }; 4179 /** 4180 * Returns a new segment object with properties and 4181 * the parts array merged. 4182 * 4183 * @param {Object} a the old segment 4184 * @param {Object} b the new segment 4185 * 4186 * @return {Object} the merged segment 4187 */ 4188 4189 4190 const updateSegment = (a, b) => { 4191 if (!a) { 4192 return b; 4193 } 4194 4195 const result = merge$1(a, b); // if only the old segment has preload hints 4196 // and the new one does not, remove preload hints. 4197 4198 if (a.preloadHints && !b.preloadHints) { 4199 delete result.preloadHints; 4200 } // if only the old segment has parts 4201 // then the parts are no longer valid 4202 4203 4204 if (a.parts && !b.parts) { 4205 delete result.parts; // if both segments have parts 4206 // copy part propeties from the old segment 4207 // to the new one. 4208 } else if (a.parts && b.parts) { 4209 for (let i = 0; i < b.parts.length; i++) { 4210 if (a.parts && a.parts[i]) { 4211 result.parts[i] = merge$1(a.parts[i], b.parts[i]); 4212 } 4213 } 4214 } // set skipped to false for segments that have 4215 // have had information merged from the old segment. 4216 4217 4218 if (!a.skipped && b.skipped) { 4219 result.skipped = false; 4220 } // set preload to false for segments that have 4221 // had information added in the new segment. 4222 4223 4224 if (a.preload && !b.preload) { 4225 result.preload = false; 4226 } 4227 4228 return result; 4229 }; 4230 /** 4231 * Returns a new array of segments that is the result of merging 4232 * properties from an older list of segments onto an updated 4233 * list. No properties on the updated playlist will be ovewritten. 4234 * 4235 * @param {Array} original the outdated list of segments 4236 * @param {Array} update the updated list of segments 4237 * @param {number=} offset the index of the first update 4238 * segment in the original segment list. For non-live playlists, 4239 * this should always be zero and does not need to be 4240 * specified. For live playlists, it should be the difference 4241 * between the media sequence numbers in the original and updated 4242 * playlists. 4243 * @return {Array} a list of merged segment objects 4244 */ 4245 4246 const updateSegments = (original, update, offset) => { 4247 const oldSegments = original.slice(); 4248 const newSegments = update.slice(); 4249 offset = offset || 0; 4250 const result = []; 4251 let currentMap; 4252 4253 for (let newIndex = 0; newIndex < newSegments.length; newIndex++) { 4254 const oldSegment = oldSegments[newIndex + offset]; 4255 const newSegment = newSegments[newIndex]; 4256 4257 if (oldSegment) { 4258 currentMap = oldSegment.map || currentMap; 4259 result.push(updateSegment(oldSegment, newSegment)); 4260 } else { 4261 // carry over map to new segment if it is missing 4262 if (currentMap && !newSegment.map) { 4263 newSegment.map = currentMap; 4264 } 4265 4266 result.push(newSegment); 4267 } 4268 } 4269 4270 return result; 4271 }; 4272 const resolveSegmentUris = (segment, baseUri) => { 4273 // preloadSegment will not have a uri at all 4274 // as the segment isn't actually in the manifest yet, only parts 4275 if (!segment.resolvedUri && segment.uri) { 4276 segment.resolvedUri = resolveUrl(baseUri, segment.uri); 4277 } 4278 4279 if (segment.key && !segment.key.resolvedUri) { 4280 segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri); 4281 } 4282 4283 if (segment.map && !segment.map.resolvedUri) { 4284 segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri); 4285 } 4286 4287 if (segment.map && segment.map.key && !segment.map.key.resolvedUri) { 4288 segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri); 4289 } 4290 4291 if (segment.parts && segment.parts.length) { 4292 segment.parts.forEach(p => { 4293 if (p.resolvedUri) { 4294 return; 4295 } 4296 4297 p.resolvedUri = resolveUrl(baseUri, p.uri); 4298 }); 4299 } 4300 4301 if (segment.preloadHints && segment.preloadHints.length) { 4302 segment.preloadHints.forEach(p => { 4303 if (p.resolvedUri) { 4304 return; 4305 } 4306 4307 p.resolvedUri = resolveUrl(baseUri, p.uri); 4308 }); 4309 } 4310 }; 4311 4312 const getAllSegments = function (media) { 4313 const segments = media.segments || []; 4314 const preloadSegment = media.preloadSegment; // a preloadSegment with only preloadHints is not currently 4315 // a usable segment, only include a preloadSegment that has 4316 // parts. 4317 4318 if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) { 4319 // if preloadHints has a MAP that means that the 4320 // init segment is going to change. We cannot use any of the parts 4321 // from this preload segment. 4322 if (preloadSegment.preloadHints) { 4323 for (let i = 0; i < preloadSegment.preloadHints.length; i++) { 4324 if (preloadSegment.preloadHints[i].type === 'MAP') { 4325 return segments; 4326 } 4327 } 4328 } // set the duration for our preload segment to target duration. 4329 4330 4331 preloadSegment.duration = media.targetDuration; 4332 preloadSegment.preload = true; 4333 segments.push(preloadSegment); 4334 } 4335 4336 return segments; 4337 }; // consider the playlist unchanged if the playlist object is the same or 4338 // the number of segments is equal, the media sequence number is unchanged, 4339 // and this playlist hasn't become the end of the playlist 4340 4341 4342 const isPlaylistUnchanged = (a, b) => a === b || a.segments && b.segments && a.segments.length === b.segments.length && a.endList === b.endList && a.mediaSequence === b.mediaSequence && a.preloadSegment === b.preloadSegment; 4343 /** 4344 * Returns a new main playlist that is the result of merging an 4345 * updated media playlist into the original version. If the 4346 * updated media playlist does not match any of the playlist 4347 * entries in the original main playlist, null is returned. 4348 * 4349 * @param {Object} main a parsed main M3U8 object 4350 * @param {Object} media a parsed media M3U8 object 4351 * @return {Object} a new object that represents the original 4352 * main playlist with the updated media playlist merged in, or 4353 * null if the merge produced no change. 4354 */ 4355 4356 const updateMain$1 = (main, newMedia, unchangedCheck = isPlaylistUnchanged) => { 4357 const result = merge$1(main, {}); 4358 const oldMedia = result.playlists[newMedia.id]; 4359 4360 if (!oldMedia) { 4361 return null; 4362 } 4363 4364 if (unchangedCheck(oldMedia, newMedia)) { 4365 return null; 4366 } 4367 4368 newMedia.segments = getAllSegments(newMedia); 4369 const mergedPlaylist = merge$1(oldMedia, newMedia); // always use the new media's preload segment 4370 4371 if (mergedPlaylist.preloadSegment && !newMedia.preloadSegment) { 4372 delete mergedPlaylist.preloadSegment; 4373 } // if the update could overlap existing segment information, merge the two segment lists 4374 4375 4376 if (oldMedia.segments) { 4377 if (newMedia.skip) { 4378 newMedia.segments = newMedia.segments || []; // add back in objects for skipped segments, so that we merge 4379 // old properties into the new segments 4380 4381 for (let i = 0; i < newMedia.skip.skippedSegments; i++) { 4382 newMedia.segments.unshift({ 4383 skipped: true 4384 }); 4385 } 4386 } 4387 4388 mergedPlaylist.segments = updateSegments(oldMedia.segments, newMedia.segments, newMedia.mediaSequence - oldMedia.mediaSequence); 4389 } // resolve any segment URIs to prevent us from having to do it later 4390 4391 4392 mergedPlaylist.segments.forEach(segment => { 4393 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 4394 }); // TODO Right now in the playlists array there are two references to each playlist, one 4395 // that is referenced by index, and one by URI. The index reference may no longer be 4396 // necessary. 4397 4398 for (let i = 0; i < result.playlists.length; i++) { 4399 if (result.playlists[i].id === newMedia.id) { 4400 result.playlists[i] = mergedPlaylist; 4401 } 4402 } 4403 4404 result.playlists[newMedia.id] = mergedPlaylist; // URI reference added for backwards compatibility 4405 4406 result.playlists[newMedia.uri] = mergedPlaylist; // update media group playlist references. 4407 4408 forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => { 4409 if (!properties.playlists) { 4410 return; 4411 } 4412 4413 for (let i = 0; i < properties.playlists.length; i++) { 4414 if (newMedia.id === properties.playlists[i].id) { 4415 properties.playlists[i] = mergedPlaylist; 4416 } 4417 } 4418 }); 4419 return result; 4420 }; 4421 /** 4422 * Calculates the time to wait before refreshing a live playlist 4423 * 4424 * @param {Object} media 4425 * The current media 4426 * @param {boolean} update 4427 * True if there were any updates from the last refresh, false otherwise 4428 * @return {number} 4429 * The time in ms to wait before refreshing the live playlist 4430 */ 4431 4432 const refreshDelay = (media, update) => { 4433 const segments = media.segments || []; 4434 const lastSegment = segments[segments.length - 1]; 4435 const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1]; 4436 const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration; 4437 4438 if (update && lastDuration) { 4439 return lastDuration * 1000; 4440 } // if the playlist is unchanged since the last reload or last segment duration 4441 // cannot be determined, try again after half the target duration 4442 4443 4444 return (media.partTargetDuration || media.targetDuration || 10) * 500; 4445 }; 4446 4447 const playlistMetadataPayload = (playlists, type, isLive) => { 4448 if (!playlists) { 4449 return; 4450 } 4451 4452 const renditions = []; 4453 playlists.forEach(playlist => { 4454 // we need attributes to populate rendition data. 4455 if (!playlist.attributes) { 4456 return; 4457 } 4458 4459 const { 4460 BANDWIDTH, 4461 RESOLUTION, 4462 CODECS 4463 } = playlist.attributes; 4464 renditions.push({ 4465 id: playlist.id, 4466 bandwidth: BANDWIDTH, 4467 resolution: RESOLUTION, 4468 codecs: CODECS 4469 }); 4470 }); 4471 return { 4472 type, 4473 isLive, 4474 renditions 4475 }; 4476 }; 4477 /** 4478 * Load a playlist from a remote location 4479 * 4480 * @class PlaylistLoader 4481 * @extends Stream 4482 * @param {string|Object} src url or object of manifest 4483 * @param {boolean} withCredentials the withCredentials xhr option 4484 * @class 4485 */ 4486 4487 4488 class PlaylistLoader extends EventTarget$1 { 4489 constructor(src, vhs, options = {}) { 4490 super(); 4491 4492 if (!src) { 4493 throw new Error('A non-empty playlist URL or object is required'); 4494 } 4495 4496 this.logger_ = logger('PlaylistLoader'); 4497 const { 4498 withCredentials = false 4499 } = options; 4500 this.src = src; 4501 this.vhs_ = vhs; 4502 this.withCredentials = withCredentials; 4503 this.addDateRangesToTextTrack_ = options.addDateRangesToTextTrack; 4504 const vhsOptions = vhs.options_; 4505 this.customTagParsers = vhsOptions && vhsOptions.customTagParsers || []; 4506 this.customTagMappers = vhsOptions && vhsOptions.customTagMappers || []; 4507 this.llhls = vhsOptions && vhsOptions.llhls; 4508 this.dateRangesStorage_ = new DateRangesStorage(); // initialize the loader state 4509 4510 this.state = 'HAVE_NOTHING'; // live playlist staleness timeout 4511 4512 this.handleMediaupdatetimeout_ = this.handleMediaupdatetimeout_.bind(this); 4513 this.on('mediaupdatetimeout', this.handleMediaupdatetimeout_); 4514 this.on('loadedplaylist', this.handleLoadedPlaylist_.bind(this)); 4515 } 4516 4517 handleLoadedPlaylist_() { 4518 const mediaPlaylist = this.media(); 4519 4520 if (!mediaPlaylist) { 4521 return; 4522 } 4523 4524 this.dateRangesStorage_.setOffset(mediaPlaylist.segments); 4525 this.dateRangesStorage_.setPendingDateRanges(mediaPlaylist.dateRanges); 4526 const availableDateRanges = this.dateRangesStorage_.getDateRangesToProcess(); 4527 4528 if (!availableDateRanges.length || !this.addDateRangesToTextTrack_) { 4529 return; 4530 } 4531 4532 this.addDateRangesToTextTrack_(availableDateRanges); 4533 } 4534 4535 handleMediaupdatetimeout_() { 4536 if (this.state !== 'HAVE_METADATA') { 4537 // only refresh the media playlist if no other activity is going on 4538 return; 4539 } 4540 4541 const media = this.media(); 4542 let uri = resolveUrl(this.main.uri, media.uri); 4543 4544 if (this.llhls) { 4545 uri = addLLHLSQueryDirectives(uri, media); 4546 } 4547 4548 this.state = 'HAVE_CURRENT_METADATA'; 4549 this.request = this.vhs_.xhr({ 4550 uri, 4551 withCredentials: this.withCredentials, 4552 requestType: 'hls-playlist' 4553 }, (error, req) => { 4554 // disposed 4555 if (!this.request) { 4556 return; 4557 } 4558 4559 if (error) { 4560 return this.playlistRequestError(this.request, this.media(), 'HAVE_METADATA'); 4561 } 4562 4563 this.haveMetadata({ 4564 playlistString: this.request.responseText, 4565 url: this.media().uri, 4566 id: this.media().id 4567 }); 4568 }); 4569 } 4570 4571 playlistRequestError(xhr, playlist, startingState) { 4572 const { 4573 uri, 4574 id 4575 } = playlist; // any in-flight request is now finished 4576 4577 this.request = null; 4578 4579 if (startingState) { 4580 this.state = startingState; 4581 } 4582 4583 this.error = { 4584 playlist: this.main.playlists[id], 4585 status: xhr.status, 4586 message: `HLS playlist request error at URL: ${uri}.`, 4587 responseText: xhr.responseText, 4588 code: xhr.status >= 500 ? 4 : 2, 4589 metadata: getStreamingNetworkErrorMetadata({ 4590 requestType: xhr.requestType, 4591 request: xhr, 4592 error: xhr.error 4593 }) 4594 }; 4595 this.trigger('error'); 4596 } 4597 4598 parseManifest_({ 4599 url, 4600 manifestString 4601 }) { 4602 try { 4603 const parsed = parseManifest({ 4604 onwarn: ({ 4605 message 4606 }) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), 4607 oninfo: ({ 4608 message 4609 }) => this.logger_(`m3u8-parser info for ${url}: ${message}`), 4610 manifestString, 4611 customTagParsers: this.customTagParsers, 4612 customTagMappers: this.customTagMappers, 4613 llhls: this.llhls 4614 }); 4615 /** 4616 * VHS does not support switching between variants with and without audio and video 4617 * so we want to filter out audio-only variants when variants with video and(or) audio are also detected. 4618 */ 4619 4620 if (!parsed.playlists || !parsed.playlists.length) { 4621 return parsed; 4622 } 4623 4624 this.excludeAudioOnlyVariants(parsed.playlists); 4625 return parsed; 4626 } catch (error) { 4627 this.error = error; 4628 this.error.metadata = { 4629 errorType: videojs__default["default"].Error.StreamingHlsPlaylistParserError, 4630 error 4631 }; 4632 } 4633 } 4634 4635 excludeAudioOnlyVariants(playlists) { 4636 // helper function 4637 const hasVideo = playlist => { 4638 const attributes = playlist.attributes || {}; 4639 const { 4640 width, 4641 height 4642 } = attributes.RESOLUTION || {}; 4643 4644 if (width && height) { 4645 return true; 4646 } // parse codecs string from playlist attributes 4647 4648 4649 const codecsList = getCodecs(playlist) || []; // unwrap list 4650 4651 const codecsInfo = unwrapCodecList(codecsList); 4652 return Boolean(codecsInfo.video); 4653 }; 4654 4655 if (playlists.some(hasVideo)) { 4656 playlists.forEach(playlist => { 4657 if (!hasVideo(playlist)) { 4658 playlist.excludeUntil = Infinity; 4659 } 4660 }); 4661 } 4662 } 4663 /** 4664 * Update the playlist loader's state in response to a new or updated playlist. 4665 * 4666 * @param {string} [playlistString] 4667 * Playlist string (if playlistObject is not provided) 4668 * @param {Object} [playlistObject] 4669 * Playlist object (if playlistString is not provided) 4670 * @param {string} url 4671 * URL of playlist 4672 * @param {string} id 4673 * ID to use for playlist 4674 */ 4675 4676 4677 haveMetadata({ 4678 playlistString, 4679 playlistObject, 4680 url, 4681 id 4682 }) { 4683 // any in-flight request is now finished 4684 this.request = null; 4685 this.state = 'HAVE_METADATA'; 4686 const metadata = { 4687 playlistInfo: { 4688 type: 'media', 4689 uri: url 4690 } 4691 }; 4692 this.trigger({ 4693 type: 'playlistparsestart', 4694 metadata 4695 }); 4696 const playlist = playlistObject || this.parseManifest_({ 4697 url, 4698 manifestString: playlistString 4699 }); 4700 playlist.lastRequest = Date.now(); 4701 setupMediaPlaylist({ 4702 playlist, 4703 uri: url, 4704 id 4705 }); // merge this playlist into the main manifest 4706 4707 const update = updateMain$1(this.main, playlist); 4708 this.targetDuration = playlist.partTargetDuration || playlist.targetDuration; 4709 this.pendingMedia_ = null; 4710 4711 if (update) { 4712 this.main = update; 4713 this.media_ = this.main.playlists[id]; 4714 } else { 4715 this.trigger('playlistunchanged'); 4716 } 4717 4718 this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update)); 4719 metadata.parsedPlaylist = playlistMetadataPayload(this.main.playlists, metadata.playlistInfo.type, !this.media_.endList); 4720 this.trigger({ 4721 type: 'playlistparsecomplete', 4722 metadata 4723 }); 4724 this.trigger('loadedplaylist'); 4725 } 4726 /** 4727 * Abort any outstanding work and clean up. 4728 */ 4729 4730 4731 dispose() { 4732 this.trigger('dispose'); 4733 this.stopRequest(); 4734 window.clearTimeout(this.mediaUpdateTimeout); 4735 window.clearTimeout(this.finalRenditionTimeout); 4736 this.dateRangesStorage_ = new DateRangesStorage(); 4737 this.off(); 4738 } 4739 4740 stopRequest() { 4741 if (this.request) { 4742 const oldRequest = this.request; 4743 this.request = null; 4744 oldRequest.onreadystatechange = null; 4745 oldRequest.abort(); 4746 } 4747 } 4748 /** 4749 * When called without any arguments, returns the currently 4750 * active media playlist. When called with a single argument, 4751 * triggers the playlist loader to asynchronously switch to the 4752 * specified media playlist. Calling this method while the 4753 * loader is in the HAVE_NOTHING causes an error to be emitted 4754 * but otherwise has no effect. 4755 * 4756 * @param {Object=} playlist the parsed media playlist 4757 * object to switch to 4758 * @param {boolean=} shouldDelay whether we should delay the request by half target duration 4759 * 4760 * @return {Playlist} the current loaded media 4761 */ 4762 4763 4764 media(playlist, shouldDelay) { 4765 // getter 4766 if (!playlist) { 4767 return this.media_; 4768 } // setter 4769 4770 4771 if (this.state === 'HAVE_NOTHING') { 4772 throw new Error('Cannot switch media playlist from ' + this.state); 4773 } // find the playlist object if the target playlist has been 4774 // specified by URI 4775 4776 4777 if (typeof playlist === 'string') { 4778 if (!this.main.playlists[playlist]) { 4779 throw new Error('Unknown playlist URI: ' + playlist); 4780 } 4781 4782 playlist = this.main.playlists[playlist]; 4783 } 4784 4785 window.clearTimeout(this.finalRenditionTimeout); 4786 4787 if (shouldDelay) { 4788 const delay = (playlist.partTargetDuration || playlist.targetDuration) / 2 * 1000 || 5 * 1000; 4789 this.finalRenditionTimeout = window.setTimeout(this.media.bind(this, playlist, false), delay); 4790 return; 4791 } 4792 4793 const startingState = this.state; 4794 const mediaChange = !this.media_ || playlist.id !== this.media_.id; 4795 const mainPlaylistRef = this.main.playlists[playlist.id]; // switch to fully loaded playlists immediately 4796 4797 if (mainPlaylistRef && mainPlaylistRef.endList || // handle the case of a playlist object (e.g., if using vhs-json with a resolved 4798 // media playlist or, for the case of demuxed audio, a resolved audio media group) 4799 playlist.endList && playlist.segments.length) { 4800 // abort outstanding playlist requests 4801 if (this.request) { 4802 this.request.onreadystatechange = null; 4803 this.request.abort(); 4804 this.request = null; 4805 } 4806 4807 this.state = 'HAVE_METADATA'; 4808 this.media_ = playlist; // trigger media change if the active media has been updated 4809 4810 if (mediaChange) { 4811 this.trigger('mediachanging'); 4812 4813 if (startingState === 'HAVE_MAIN_MANIFEST') { 4814 // The initial playlist was a main manifest, and the first media selected was 4815 // also provided (in the form of a resolved playlist object) as part of the 4816 // source object (rather than just a URL). Therefore, since the media playlist 4817 // doesn't need to be requested, loadedmetadata won't trigger as part of the 4818 // normal flow, and needs an explicit trigger here. 4819 this.trigger('loadedmetadata'); 4820 } else { 4821 this.trigger('mediachange'); 4822 } 4823 } 4824 4825 return; 4826 } // We update/set the timeout here so that live playlists 4827 // that are not a media change will "start" the loader as expected. 4828 // We expect that this function will start the media update timeout 4829 // cycle again. This also prevents a playlist switch failure from 4830 // causing us to stall during live. 4831 4832 4833 this.updateMediaUpdateTimeout_(refreshDelay(playlist, true)); // switching to the active playlist is a no-op 4834 4835 if (!mediaChange) { 4836 return; 4837 } 4838 4839 this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request 4840 4841 if (this.request) { 4842 if (playlist.resolvedUri === this.request.url) { 4843 // requesting to switch to the same playlist multiple times 4844 // has no effect after the first 4845 return; 4846 } 4847 4848 this.request.onreadystatechange = null; 4849 this.request.abort(); 4850 this.request = null; 4851 } // request the new playlist 4852 4853 4854 if (this.media_) { 4855 this.trigger('mediachanging'); 4856 } 4857 4858 this.pendingMedia_ = playlist; 4859 const metadata = { 4860 playlistInfo: { 4861 type: 'media', 4862 uri: playlist.uri 4863 } 4864 }; 4865 this.trigger({ 4866 type: 'playlistrequeststart', 4867 metadata 4868 }); 4869 this.request = this.vhs_.xhr({ 4870 uri: playlist.resolvedUri, 4871 withCredentials: this.withCredentials, 4872 requestType: 'hls-playlist' 4873 }, (error, req) => { 4874 // disposed 4875 if (!this.request) { 4876 return; 4877 } 4878 4879 playlist.lastRequest = Date.now(); 4880 playlist.resolvedUri = resolveManifestRedirect(playlist.resolvedUri, req); 4881 4882 if (error) { 4883 return this.playlistRequestError(this.request, playlist, startingState); 4884 } 4885 4886 this.trigger({ 4887 type: 'playlistrequestcomplete', 4888 metadata 4889 }); 4890 this.haveMetadata({ 4891 playlistString: req.responseText, 4892 url: playlist.uri, 4893 id: playlist.id 4894 }); // fire loadedmetadata the first time a media playlist is loaded 4895 4896 if (startingState === 'HAVE_MAIN_MANIFEST') { 4897 this.trigger('loadedmetadata'); 4898 } else { 4899 this.trigger('mediachange'); 4900 } 4901 }); 4902 } 4903 /** 4904 * pause loading of the playlist 4905 */ 4906 4907 4908 pause() { 4909 if (this.mediaUpdateTimeout) { 4910 window.clearTimeout(this.mediaUpdateTimeout); 4911 this.mediaUpdateTimeout = null; 4912 } 4913 4914 this.stopRequest(); 4915 4916 if (this.state === 'HAVE_NOTHING') { 4917 // If we pause the loader before any data has been retrieved, its as if we never 4918 // started, so reset to an unstarted state. 4919 this.started = false; 4920 } // Need to restore state now that no activity is happening 4921 4922 4923 if (this.state === 'SWITCHING_MEDIA') { 4924 // if the loader was in the process of switching media, it should either return to 4925 // HAVE_MAIN_MANIFEST or HAVE_METADATA depending on if the loader has loaded a media 4926 // playlist yet. This is determined by the existence of loader.media_ 4927 if (this.media_) { 4928 this.state = 'HAVE_METADATA'; 4929 } else { 4930 this.state = 'HAVE_MAIN_MANIFEST'; 4931 } 4932 } else if (this.state === 'HAVE_CURRENT_METADATA') { 4933 this.state = 'HAVE_METADATA'; 4934 } 4935 } 4936 /** 4937 * start loading of the playlist 4938 */ 4939 4940 4941 load(shouldDelay) { 4942 if (this.mediaUpdateTimeout) { 4943 window.clearTimeout(this.mediaUpdateTimeout); 4944 this.mediaUpdateTimeout = null; 4945 } 4946 4947 const media = this.media(); 4948 4949 if (shouldDelay) { 4950 const delay = media ? (media.partTargetDuration || media.targetDuration) / 2 * 1000 : 5 * 1000; 4951 this.mediaUpdateTimeout = window.setTimeout(() => { 4952 this.mediaUpdateTimeout = null; 4953 this.load(); 4954 }, delay); 4955 return; 4956 } 4957 4958 if (!this.started) { 4959 this.start(); 4960 return; 4961 } 4962 4963 if (media && !media.endList) { 4964 this.trigger('mediaupdatetimeout'); 4965 } else { 4966 this.trigger('loadedplaylist'); 4967 } 4968 } 4969 4970 updateMediaUpdateTimeout_(delay) { 4971 if (this.mediaUpdateTimeout) { 4972 window.clearTimeout(this.mediaUpdateTimeout); 4973 this.mediaUpdateTimeout = null; 4974 } // we only have use mediaupdatetimeout for live playlists. 4975 4976 4977 if (!this.media() || this.media().endList) { 4978 return; 4979 } 4980 4981 this.mediaUpdateTimeout = window.setTimeout(() => { 4982 this.mediaUpdateTimeout = null; 4983 this.trigger('mediaupdatetimeout'); 4984 this.updateMediaUpdateTimeout_(delay); 4985 }, delay); 4986 } 4987 /** 4988 * start loading of the playlist 4989 */ 4990 4991 4992 start() { 4993 this.started = true; 4994 4995 if (typeof this.src === 'object') { 4996 // in the case of an entirely constructed manifest object (meaning there's no actual 4997 // manifest on a server), default the uri to the page's href 4998 if (!this.src.uri) { 4999 this.src.uri = window.location.href; 5000 } // resolvedUri is added on internally after the initial request. Since there's no 5001 // request for pre-resolved manifests, add on resolvedUri here. 5002 5003 5004 this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first 5005 // request can be skipped (since the top level of the manifest, at a minimum, is 5006 // already available as a parsed manifest object). However, if the manifest object 5007 // represents a main playlist, some media playlists may need to be resolved before 5008 // the starting segment list is available. Therefore, go directly to setup of the 5009 // initial playlist, and let the normal flow continue from there. 5010 // 5011 // Note that the call to setup is asynchronous, as other sections of VHS may assume 5012 // that the first request is asynchronous. 5013 5014 setTimeout(() => { 5015 this.setupInitialPlaylist(this.src); 5016 }, 0); 5017 return; 5018 } 5019 5020 const metadata = { 5021 playlistInfo: { 5022 type: 'multivariant', 5023 uri: this.src 5024 } 5025 }; 5026 this.trigger({ 5027 type: 'playlistrequeststart', 5028 metadata 5029 }); // request the specified URL 5030 5031 this.request = this.vhs_.xhr({ 5032 uri: this.src, 5033 withCredentials: this.withCredentials, 5034 requestType: 'hls-playlist' 5035 }, (error, req) => { 5036 // disposed 5037 if (!this.request) { 5038 return; 5039 } // clear the loader's request reference 5040 5041 5042 this.request = null; 5043 5044 if (error) { 5045 this.error = { 5046 status: req.status, 5047 message: `HLS playlist request error at URL: ${this.src}.`, 5048 responseText: req.responseText, 5049 // MEDIA_ERR_NETWORK 5050 code: 2, 5051 metadata: getStreamingNetworkErrorMetadata({ 5052 requestType: req.requestType, 5053 request: req, 5054 error 5055 }) 5056 }; 5057 5058 if (this.state === 'HAVE_NOTHING') { 5059 this.started = false; 5060 } 5061 5062 return this.trigger('error'); 5063 } 5064 5065 this.trigger({ 5066 type: 'playlistrequestcomplete', 5067 metadata 5068 }); 5069 this.src = resolveManifestRedirect(this.src, req); 5070 this.trigger({ 5071 type: 'playlistparsestart', 5072 metadata 5073 }); 5074 const manifest = this.parseManifest_({ 5075 manifestString: req.responseText, 5076 url: this.src 5077 }); // we haven't loaded any variant playlists here so we default to false for isLive. 5078 5079 metadata.parsedPlaylist = playlistMetadataPayload(manifest.playlists, metadata.playlistInfo.type, false); 5080 this.trigger({ 5081 type: 'playlistparsecomplete', 5082 metadata 5083 }); 5084 this.setupInitialPlaylist(manifest); 5085 }); 5086 } 5087 5088 srcUri() { 5089 return typeof this.src === 'string' ? this.src : this.src.uri; 5090 } 5091 /** 5092 * Given a manifest object that's either a main or media playlist, trigger the proper 5093 * events and set the state of the playlist loader. 5094 * 5095 * If the manifest object represents a main playlist, `loadedplaylist` will be 5096 * triggered to allow listeners to select a playlist. If none is selected, the loader 5097 * will default to the first one in the playlists array. 5098 * 5099 * If the manifest object represents a media playlist, `loadedplaylist` will be 5100 * triggered followed by `loadedmetadata`, as the only available playlist is loaded. 5101 * 5102 * In the case of a media playlist, a main playlist object wrapper with one playlist 5103 * will be created so that all logic can handle playlists in the same fashion (as an 5104 * assumed manifest object schema). 5105 * 5106 * @param {Object} manifest 5107 * The parsed manifest object 5108 */ 5109 5110 5111 setupInitialPlaylist(manifest) { 5112 this.state = 'HAVE_MAIN_MANIFEST'; 5113 5114 if (manifest.playlists) { 5115 this.main = manifest; 5116 addPropertiesToMain(this.main, this.srcUri()); // If the initial main playlist has playlists wtih segments already resolved, 5117 // then resolve URIs in advance, as they are usually done after a playlist request, 5118 // which may not happen if the playlist is resolved. 5119 5120 manifest.playlists.forEach(playlist => { 5121 playlist.segments = getAllSegments(playlist); 5122 playlist.segments.forEach(segment => { 5123 resolveSegmentUris(segment, playlist.resolvedUri); 5124 }); 5125 }); 5126 this.trigger('loadedplaylist'); 5127 5128 if (!this.request) { 5129 // no media playlist was specifically selected so start 5130 // from the first listed one 5131 this.media(this.main.playlists[0]); 5132 } 5133 5134 return; 5135 } // In order to support media playlists passed in as vhs-json, the case where the uri 5136 // is not provided as part of the manifest should be considered, and an appropriate 5137 // default used. 5138 5139 5140 const uri = this.srcUri() || window.location.href; 5141 this.main = mainForMedia(manifest, uri); 5142 this.haveMetadata({ 5143 playlistObject: manifest, 5144 url: uri, 5145 id: this.main.playlists[0].id 5146 }); 5147 this.trigger('loadedmetadata'); 5148 } 5149 /** 5150 * Updates or deletes a preexisting pathway clone. 5151 * Ensures that all playlists related to the old pathway clone are 5152 * either updated or deleted. 5153 * 5154 * @param {Object} clone On update, the pathway clone object for the newly updated pathway clone. 5155 * On delete, the old pathway clone object to be deleted. 5156 * @param {boolean} isUpdate True if the pathway is to be updated, 5157 * false if it is meant to be deleted. 5158 */ 5159 5160 5161 updateOrDeleteClone(clone, isUpdate) { 5162 const main = this.main; 5163 const pathway = clone.ID; 5164 let i = main.playlists.length; // Iterate backwards through the playlist so we can remove playlists if necessary. 5165 5166 while (i--) { 5167 const p = main.playlists[i]; 5168 5169 if (p.attributes['PATHWAY-ID'] === pathway) { 5170 const oldPlaylistUri = p.resolvedUri; 5171 const oldPlaylistId = p.id; // update the indexed playlist and add new playlists by ID and URI 5172 5173 if (isUpdate) { 5174 const newPlaylistUri = this.createCloneURI_(p.resolvedUri, clone); 5175 const newPlaylistId = createPlaylistID(pathway, newPlaylistUri); 5176 const attributes = this.createCloneAttributes_(pathway, p.attributes); 5177 const updatedPlaylist = this.createClonePlaylist_(p, newPlaylistId, clone, attributes); 5178 main.playlists[i] = updatedPlaylist; 5179 main.playlists[newPlaylistId] = updatedPlaylist; 5180 main.playlists[newPlaylistUri] = updatedPlaylist; 5181 } else { 5182 // Remove the indexed playlist. 5183 main.playlists.splice(i, 1); 5184 } // Remove playlists by the old ID and URI. 5185 5186 5187 delete main.playlists[oldPlaylistId]; 5188 delete main.playlists[oldPlaylistUri]; 5189 } 5190 } 5191 5192 this.updateOrDeleteCloneMedia(clone, isUpdate); 5193 } 5194 /** 5195 * Updates or deletes media data based on the pathway clone object. 5196 * Due to the complexity of the media groups and playlists, in all cases 5197 * we remove all of the old media groups and playlists. 5198 * On updates, we then create new media groups and playlists based on the 5199 * new pathway clone object. 5200 * 5201 * @param {Object} clone The pathway clone object for the newly updated pathway clone. 5202 * @param {boolean} isUpdate True if the pathway is to be updated, 5203 * false if it is meant to be deleted. 5204 */ 5205 5206 5207 updateOrDeleteCloneMedia(clone, isUpdate) { 5208 const main = this.main; 5209 const id = clone.ID; 5210 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 5211 if (!main.mediaGroups[mediaType] || !main.mediaGroups[mediaType][id]) { 5212 return; 5213 } 5214 5215 for (const groupKey in main.mediaGroups[mediaType]) { 5216 // Remove all media playlists for the media group for this pathway clone. 5217 if (groupKey === id) { 5218 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 5219 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 5220 oldMedia.playlists.forEach((p, i) => { 5221 const oldMediaPlaylist = main.playlists[p.id]; 5222 const oldPlaylistId = oldMediaPlaylist.id; 5223 const oldPlaylistUri = oldMediaPlaylist.resolvedUri; 5224 delete main.playlists[oldPlaylistId]; 5225 delete main.playlists[oldPlaylistUri]; 5226 }); 5227 } // Delete the old media group. 5228 5229 5230 delete main.mediaGroups[mediaType][groupKey]; 5231 } 5232 } 5233 }); // Create the new media groups and playlists if there is an update. 5234 5235 if (isUpdate) { 5236 this.createClonedMediaGroups_(clone); 5237 } 5238 } 5239 /** 5240 * Given a pathway clone object, clones all necessary playlists. 5241 * 5242 * @param {Object} clone The pathway clone object. 5243 * @param {Object} basePlaylist The original playlist to clone from. 5244 */ 5245 5246 5247 addClonePathway(clone, basePlaylist = {}) { 5248 const main = this.main; 5249 const index = main.playlists.length; 5250 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 5251 const playlistId = createPlaylistID(clone.ID, uri); 5252 const attributes = this.createCloneAttributes_(clone.ID, basePlaylist.attributes); 5253 const playlist = this.createClonePlaylist_(basePlaylist, playlistId, clone, attributes); 5254 main.playlists[index] = playlist; // add playlist by ID and URI 5255 5256 main.playlists[playlistId] = playlist; 5257 main.playlists[uri] = playlist; 5258 this.createClonedMediaGroups_(clone); 5259 } 5260 /** 5261 * Given a pathway clone object we create clones of all media. 5262 * In this function, all necessary information and updated playlists 5263 * are added to the `mediaGroup` object. 5264 * Playlists are also added to the `playlists` array so the media groups 5265 * will be properly linked. 5266 * 5267 * @param {Object} clone The pathway clone object. 5268 */ 5269 5270 5271 createClonedMediaGroups_(clone) { 5272 const id = clone.ID; 5273 const baseID = clone['BASE-ID']; 5274 const main = this.main; 5275 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 5276 // If the media type doesn't exist, or there is already a clone, skip 5277 // to the next media type. 5278 if (!main.mediaGroups[mediaType] || main.mediaGroups[mediaType][id]) { 5279 return; 5280 } 5281 5282 for (const groupKey in main.mediaGroups[mediaType]) { 5283 if (groupKey === baseID) { 5284 // Create the group. 5285 main.mediaGroups[mediaType][id] = {}; 5286 } else { 5287 // There is no need to iterate over label keys in this case. 5288 continue; 5289 } 5290 5291 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 5292 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 5293 main.mediaGroups[mediaType][id][labelKey] = _extends({}, oldMedia); 5294 const newMedia = main.mediaGroups[mediaType][id][labelKey]; // update URIs on the media 5295 5296 const newUri = this.createCloneURI_(oldMedia.resolvedUri, clone); 5297 newMedia.resolvedUri = newUri; 5298 newMedia.uri = newUri; // Reset playlists in the new media group. 5299 5300 newMedia.playlists = []; // Create new playlists in the newly cloned media group. 5301 5302 oldMedia.playlists.forEach((p, i) => { 5303 const oldMediaPlaylist = main.playlists[p.id]; 5304 const group = groupID(mediaType, id, labelKey); 5305 const newPlaylistID = createPlaylistID(id, group); // Check to see if it already exists 5306 5307 if (oldMediaPlaylist && !main.playlists[newPlaylistID]) { 5308 const newMediaPlaylist = this.createClonePlaylist_(oldMediaPlaylist, newPlaylistID, clone); 5309 const newPlaylistUri = newMediaPlaylist.resolvedUri; 5310 main.playlists[newPlaylistID] = newMediaPlaylist; 5311 main.playlists[newPlaylistUri] = newMediaPlaylist; 5312 } 5313 5314 newMedia.playlists[i] = this.createClonePlaylist_(p, newPlaylistID, clone); 5315 }); 5316 } 5317 } 5318 }); 5319 } 5320 /** 5321 * Using the original playlist to be cloned, and the pathway clone object 5322 * information, we create a new playlist. 5323 * 5324 * @param {Object} basePlaylist The original playlist to be cloned from. 5325 * @param {string} id The desired id of the newly cloned playlist. 5326 * @param {Object} clone The pathway clone object. 5327 * @param {Object} attributes An optional object to populate the `attributes` property in the playlist. 5328 * 5329 * @return {Object} The combined cloned playlist. 5330 */ 5331 5332 5333 createClonePlaylist_(basePlaylist, id, clone, attributes) { 5334 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 5335 const newProps = { 5336 resolvedUri: uri, 5337 uri, 5338 id 5339 }; // Remove all segments from previous playlist in the clone. 5340 5341 if (basePlaylist.segments) { 5342 newProps.segments = []; 5343 } 5344 5345 if (attributes) { 5346 newProps.attributes = attributes; 5347 } 5348 5349 return merge$1(basePlaylist, newProps); 5350 } 5351 /** 5352 * Generates an updated URI for a cloned pathway based on the original 5353 * pathway's URI and the paramaters from the pathway clone object in the 5354 * content steering server response. 5355 * 5356 * @param {string} baseUri URI to be updated in the cloned pathway. 5357 * @param {Object} clone The pathway clone object. 5358 * 5359 * @return {string} The updated URI for the cloned pathway. 5360 */ 5361 5362 5363 createCloneURI_(baseURI, clone) { 5364 const uri = new URL(baseURI); 5365 uri.hostname = clone['URI-REPLACEMENT'].HOST; 5366 const params = clone['URI-REPLACEMENT'].PARAMS; // Add params to the cloned URL. 5367 5368 for (const key of Object.keys(params)) { 5369 uri.searchParams.set(key, params[key]); 5370 } 5371 5372 return uri.href; 5373 } 5374 /** 5375 * Helper function to create the attributes needed for the new clone. 5376 * This mainly adds the necessary media attributes. 5377 * 5378 * @param {string} id The pathway clone object ID. 5379 * @param {Object} oldAttributes The old attributes to compare to. 5380 * @return {Object} The new attributes to add to the playlist. 5381 */ 5382 5383 5384 createCloneAttributes_(id, oldAttributes) { 5385 const attributes = { 5386 ['PATHWAY-ID']: id 5387 }; 5388 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 5389 if (oldAttributes[mediaType]) { 5390 attributes[mediaType] = id; 5391 } 5392 }); 5393 return attributes; 5394 } 5395 /** 5396 * Returns the key ID set from a playlist 5397 * 5398 * @param {playlist} playlist to fetch the key ID set from. 5399 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 5400 */ 5401 5402 5403 getKeyIdSet(playlist) { 5404 const keyIds = new Set(); 5405 5406 if (!playlist || !playlist.contentProtection) { 5407 return keyIds; 5408 } 5409 5410 for (const keysystem in playlist.contentProtection) { 5411 if (playlist.contentProtection[keysystem] && playlist.contentProtection[keysystem].attributes && playlist.contentProtection[keysystem].attributes.keyId) { 5412 const keyId = playlist.contentProtection[keysystem].attributes.keyId; 5413 keyIds.add(keyId.toLowerCase()); 5414 } 5415 } 5416 5417 return keyIds; 5418 } 5419 5420 } 5421 5422 /** 5423 * @file xhr.js 5424 */ 5425 5426 const callbackWrapper = function (request, error, response, callback) { 5427 const reqResponse = request.responseType === 'arraybuffer' ? request.response : request.responseText; 5428 5429 if (!error && reqResponse) { 5430 request.responseTime = Date.now(); 5431 request.roundTripTime = request.responseTime - request.requestTime; 5432 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 5433 5434 if (!request.bandwidth) { 5435 request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000); 5436 } 5437 } 5438 5439 if (response.headers) { 5440 request.responseHeaders = response.headers; 5441 } // videojs.xhr now uses a specific code on the error 5442 // object to signal that a request has timed out instead 5443 // of setting a boolean on the request object 5444 5445 5446 if (error && error.code === 'ETIMEDOUT') { 5447 request.timedout = true; 5448 } // videojs.xhr no longer considers status codes outside of 200 and 0 5449 // (for file uris) to be errors, but the old XHR did, so emulate that 5450 // behavior. Status 206 may be used in response to byterange requests. 5451 5452 5453 if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) { 5454 error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText))); 5455 } 5456 5457 callback(error, request); 5458 }; 5459 /** 5460 * Iterates over the request hooks Set and calls them in order 5461 * 5462 * @param {Set} hooks the hook Set to iterate over 5463 * @param {Object} options the request options to pass to the xhr wrapper 5464 * @return the callback hook function return value, the modified or new options Object. 5465 */ 5466 5467 5468 const callAllRequestHooks = (requestSet, options) => { 5469 if (!requestSet || !requestSet.size) { 5470 return; 5471 } 5472 5473 let newOptions = options; 5474 requestSet.forEach(requestCallback => { 5475 newOptions = requestCallback(newOptions); 5476 }); 5477 return newOptions; 5478 }; 5479 /** 5480 * Iterates over the response hooks Set and calls them in order. 5481 * 5482 * @param {Set} hooks the hook Set to iterate over 5483 * @param {Object} request the xhr request object 5484 * @param {Object} error the xhr error object 5485 * @param {Object} response the xhr response object 5486 */ 5487 5488 5489 const callAllResponseHooks = (responseSet, request, error, response) => { 5490 if (!responseSet || !responseSet.size) { 5491 return; 5492 } 5493 5494 responseSet.forEach(responseCallback => { 5495 responseCallback(request, error, response); 5496 }); 5497 }; 5498 5499 const xhrFactory = function () { 5500 const xhr = function XhrFunction(options, callback) { 5501 // Add a default timeout 5502 options = merge$1({ 5503 timeout: 45e3 5504 }, options); // Allow an optional user-specified function to modify the option 5505 // object before we construct the xhr request 5506 // TODO: Remove beforeRequest in the next major release. 5507 5508 const beforeRequest = XhrFunction.beforeRequest || videojs__default["default"].Vhs.xhr.beforeRequest; // onRequest and onResponse hooks as a Set, at either the player or global level. 5509 // TODO: new Set added here for beforeRequest alias. Remove this when beforeRequest is removed. 5510 5511 const _requestCallbackSet = XhrFunction._requestCallbackSet || videojs__default["default"].Vhs.xhr._requestCallbackSet || new Set(); 5512 5513 const _responseCallbackSet = XhrFunction._responseCallbackSet || videojs__default["default"].Vhs.xhr._responseCallbackSet; 5514 5515 if (beforeRequest && typeof beforeRequest === 'function') { 5516 videojs__default["default"].log.warn('beforeRequest is deprecated, use onRequest instead.'); 5517 5518 _requestCallbackSet.add(beforeRequest); 5519 } // Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden 5520 // TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11 5521 5522 5523 const xhrMethod = videojs__default["default"].Vhs.xhr.original === true ? videojs__default["default"].xhr : videojs__default["default"].Vhs.xhr; // call all registered onRequest hooks, assign new options. 5524 5525 const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options); // Remove the beforeRequest function from the hooks set so stale beforeRequest functions are not called. 5526 5527 _requestCallbackSet.delete(beforeRequest); // xhrMethod will call XMLHttpRequest.open and XMLHttpRequest.send 5528 5529 5530 const request = xhrMethod(beforeRequestOptions || options, function (error, response) { 5531 // call all registered onResponse hooks 5532 callAllResponseHooks(_responseCallbackSet, request, error, response); 5533 return callbackWrapper(request, error, response, callback); 5534 }); 5535 const originalAbort = request.abort; 5536 5537 request.abort = function () { 5538 request.aborted = true; 5539 return originalAbort.apply(request, arguments); 5540 }; 5541 5542 request.uri = options.uri; 5543 request.requestType = options.requestType; 5544 request.requestTime = Date.now(); 5545 return request; 5546 }; 5547 5548 xhr.original = true; 5549 return xhr; 5550 }; 5551 /** 5552 * Turns segment byterange into a string suitable for use in 5553 * HTTP Range requests 5554 * 5555 * @param {Object} byterange - an object with two values defining the start and end 5556 * of a byte-range 5557 */ 5558 5559 5560 const byterangeStr = function (byterange) { 5561 // `byterangeEnd` is one less than `offset + length` because the HTTP range 5562 // header uses inclusive ranges 5563 let byterangeEnd; 5564 const byterangeStart = byterange.offset; 5565 5566 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 5567 byterangeEnd = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1); 5568 } else { 5569 byterangeEnd = byterange.offset + byterange.length - 1; 5570 } 5571 5572 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 5573 }; 5574 /** 5575 * Defines headers for use in the xhr request for a particular segment. 5576 * 5577 * @param {Object} segment - a simplified copy of the segmentInfo object 5578 * from SegmentLoader 5579 */ 5580 5581 const segmentXhrHeaders = function (segment) { 5582 const headers = {}; 5583 5584 if (segment.byterange) { 5585 headers.Range = byterangeStr(segment.byterange); 5586 } 5587 5588 return headers; 5589 }; 5590 5591 var MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; 5592 var DASH_REGEX = /^application\/dash\+xml/i; 5593 /** 5594 * Returns a string that describes the type of source based on a video source object's 5595 * media type. 5596 * 5597 * @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type} 5598 * 5599 * @param {string} type 5600 * Video source object media type 5601 * @return {('hls'|'dash'|'vhs-json'|null)} 5602 * VHS source type string 5603 */ 5604 5605 var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) { 5606 if (MPEGURL_REGEX.test(type)) { 5607 return 'hls'; 5608 } 5609 5610 if (DASH_REGEX.test(type)) { 5611 return 'dash'; 5612 } // Denotes the special case of a manifest object passed to http-streaming instead of a 5613 // source URL. 5614 // 5615 // See https://en.wikipedia.org/wiki/Media_type for details on specifying media types. 5616 // 5617 // In this case, vnd stands for vendor, video.js for the organization, VHS for this 5618 // project, and the +json suffix identifies the structure of the media type. 5619 5620 5621 if (type === 'application/vnd.videojs.vhs+json') { 5622 return 'vhs-json'; 5623 } 5624 5625 return null; 5626 }; 5627 5628 // const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2)); 5629 // we used to do this with log2 but BigInt does not support builtin math 5630 // Math.ceil(log2(x)); 5631 5632 5633 var countBits = function countBits(x) { 5634 return x.toString(2).length; 5635 }; // count the number of whole bytes it would take to represent a number 5636 5637 var countBytes = function countBytes(x) { 5638 return Math.ceil(countBits(x) / 8); 5639 }; 5640 var isArrayBufferView = function isArrayBufferView(obj) { 5641 if (ArrayBuffer.isView === 'function') { 5642 return ArrayBuffer.isView(obj); 5643 } 5644 5645 return obj && obj.buffer instanceof ArrayBuffer; 5646 }; 5647 var isTypedArray = function isTypedArray(obj) { 5648 return isArrayBufferView(obj); 5649 }; 5650 var toUint8 = function toUint8(bytes) { 5651 if (bytes instanceof Uint8Array) { 5652 return bytes; 5653 } 5654 5655 if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) { 5656 // any non-number or NaN leads to empty uint8array 5657 // eslint-disable-next-line 5658 if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) { 5659 bytes = 0; 5660 } else { 5661 bytes = [bytes]; 5662 } 5663 } 5664 5665 return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0); 5666 }; 5667 var BigInt = window.BigInt || Number; 5668 var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')]; 5669 (function () { 5670 var a = new Uint16Array([0xFFCC]); 5671 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 5672 5673 if (b[0] === 0xFF) { 5674 return 'big'; 5675 } 5676 5677 if (b[0] === 0xCC) { 5678 return 'little'; 5679 } 5680 5681 return 'unknown'; 5682 })(); 5683 var bytesToNumber = function bytesToNumber(bytes, _temp) { 5684 var _ref = _temp === void 0 ? {} : _temp, 5685 _ref$signed = _ref.signed, 5686 signed = _ref$signed === void 0 ? false : _ref$signed, 5687 _ref$le = _ref.le, 5688 le = _ref$le === void 0 ? false : _ref$le; 5689 5690 bytes = toUint8(bytes); 5691 var fn = le ? 'reduce' : 'reduceRight'; 5692 var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn]; 5693 var number = obj.call(bytes, function (total, byte, i) { 5694 var exponent = le ? i : Math.abs(i + 1 - bytes.length); 5695 return total + BigInt(byte) * BYTE_TABLE[exponent]; 5696 }, BigInt(0)); 5697 5698 if (signed) { 5699 var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1); 5700 number = BigInt(number); 5701 5702 if (number > max) { 5703 number -= max; 5704 number -= max; 5705 number -= BigInt(2); 5706 } 5707 } 5708 5709 return Number(number); 5710 }; 5711 var numberToBytes = function numberToBytes(number, _temp2) { 5712 var _ref2 = _temp2 === void 0 ? {} : _temp2, 5713 _ref2$le = _ref2.le, 5714 le = _ref2$le === void 0 ? false : _ref2$le; // eslint-disable-next-line 5715 5716 5717 if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) { 5718 number = 0; 5719 } 5720 5721 number = BigInt(number); 5722 var byteCount = countBytes(number); 5723 var bytes = new Uint8Array(new ArrayBuffer(byteCount)); 5724 5725 for (var i = 0; i < byteCount; i++) { 5726 var byteIndex = le ? i : Math.abs(i + 1 - bytes.length); 5727 bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF)); 5728 5729 if (number < 0) { 5730 bytes[byteIndex] = Math.abs(~bytes[byteIndex]); 5731 bytes[byteIndex] -= i === 0 ? 1 : 2; 5732 } 5733 } 5734 5735 return bytes; 5736 }; 5737 var stringToBytes = function stringToBytes(string, stringIsBytes) { 5738 if (typeof string !== 'string' && string && typeof string.toString === 'function') { 5739 string = string.toString(); 5740 } 5741 5742 if (typeof string !== 'string') { 5743 return new Uint8Array(); 5744 } // If the string already is bytes, we don't have to do this 5745 // otherwise we do this so that we split multi length characters 5746 // into individual bytes 5747 5748 5749 if (!stringIsBytes) { 5750 string = unescape(encodeURIComponent(string)); 5751 } 5752 5753 var view = new Uint8Array(string.length); 5754 5755 for (var i = 0; i < string.length; i++) { 5756 view[i] = string.charCodeAt(i); 5757 } 5758 5759 return view; 5760 }; 5761 var concatTypedArrays = function concatTypedArrays() { 5762 for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) { 5763 buffers[_key] = arguments[_key]; 5764 } 5765 5766 buffers = buffers.filter(function (b) { 5767 return b && (b.byteLength || b.length) && typeof b !== 'string'; 5768 }); 5769 5770 if (buffers.length <= 1) { 5771 // for 0 length we will return empty uint8 5772 // for 1 length we return the first uint8 5773 return toUint8(buffers[0]); 5774 } 5775 5776 var totalLen = buffers.reduce(function (total, buf, i) { 5777 return total + (buf.byteLength || buf.length); 5778 }, 0); 5779 var tempBuffer = new Uint8Array(totalLen); 5780 var offset = 0; 5781 buffers.forEach(function (buf) { 5782 buf = toUint8(buf); 5783 tempBuffer.set(buf, offset); 5784 offset += buf.byteLength; 5785 }); 5786 return tempBuffer; 5787 }; 5788 /** 5789 * Check if the bytes "b" are contained within bytes "a". 5790 * 5791 * @param {Uint8Array|Array} a 5792 * Bytes to check in 5793 * 5794 * @param {Uint8Array|Array} b 5795 * Bytes to check for 5796 * 5797 * @param {Object} options 5798 * options 5799 * 5800 * @param {Array|Uint8Array} [offset=0] 5801 * offset to use when looking at bytes in a 5802 * 5803 * @param {Array|Uint8Array} [mask=[]] 5804 * mask to use on bytes before comparison. 5805 * 5806 * @return {boolean} 5807 * If all bytes in b are inside of a, taking into account 5808 * bit masks. 5809 */ 5810 5811 var bytesMatch = function bytesMatch(a, b, _temp3) { 5812 var _ref3 = _temp3 === void 0 ? {} : _temp3, 5813 _ref3$offset = _ref3.offset, 5814 offset = _ref3$offset === void 0 ? 0 : _ref3$offset, 5815 _ref3$mask = _ref3.mask, 5816 mask = _ref3$mask === void 0 ? [] : _ref3$mask; 5817 5818 a = toUint8(a); 5819 b = toUint8(b); // ie 11 does not support uint8 every 5820 5821 var fn = b.every ? b.every : Array.prototype.every; 5822 return b.length && a.length - offset >= b.length && // ie 11 doesn't support every on uin8 5823 fn.call(b, function (bByte, i) { 5824 var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i]; 5825 return bByte === aByte; 5826 }); 5827 }; 5828 5829 /** 5830 * @file bin-utils.js 5831 */ 5832 5833 /** 5834 * convert a TimeRange to text 5835 * 5836 * @param {TimeRange} range the timerange to use for conversion 5837 * @param {number} i the iterator on the range to convert 5838 * @return {string} the range in string format 5839 */ 5840 5841 const textRange = function (range, i) { 5842 return range.start(i) + '-' + range.end(i); 5843 }; 5844 /** 5845 * format a number as hex string 5846 * 5847 * @param {number} e The number 5848 * @param {number} i the iterator 5849 * @return {string} the hex formatted number as a string 5850 */ 5851 5852 5853 const formatHexString = function (e, i) { 5854 const value = e.toString(16); 5855 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 5856 }; 5857 5858 const formatAsciiString = function (e) { 5859 if (e >= 0x20 && e < 0x7e) { 5860 return String.fromCharCode(e); 5861 } 5862 5863 return '.'; 5864 }; 5865 /** 5866 * Creates an object for sending to a web worker modifying properties that are TypedArrays 5867 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 5868 * 5869 * @param {Object} message 5870 * Object of properties and values to send to the web worker 5871 * @return {Object} 5872 * Modified message with TypedArray values expanded 5873 * @function createTransferableMessage 5874 */ 5875 5876 5877 const createTransferableMessage = function (message) { 5878 const transferable = {}; 5879 Object.keys(message).forEach(key => { 5880 const value = message[key]; 5881 5882 if (isArrayBufferView(value)) { 5883 transferable[key] = { 5884 bytes: value.buffer, 5885 byteOffset: value.byteOffset, 5886 byteLength: value.byteLength 5887 }; 5888 } else { 5889 transferable[key] = value; 5890 } 5891 }); 5892 return transferable; 5893 }; 5894 /** 5895 * Returns a unique string identifier for a media initialization 5896 * segment. 5897 * 5898 * @param {Object} initSegment 5899 * the init segment object. 5900 * 5901 * @return {string} the generated init segment id 5902 */ 5903 5904 const initSegmentId = function (initSegment) { 5905 const byterange = initSegment.byterange || { 5906 length: Infinity, 5907 offset: 0 5908 }; 5909 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 5910 }; 5911 /** 5912 * Returns a unique string identifier for a media segment key. 5913 * 5914 * @param {Object} key the encryption key 5915 * @return {string} the unique id for the media segment key. 5916 */ 5917 5918 const segmentKeyId = function (key) { 5919 return key.resolvedUri; 5920 }; 5921 /** 5922 * utils to help dump binary data to the console 5923 * 5924 * @param {Array|TypedArray} data 5925 * data to dump to a string 5926 * 5927 * @return {string} the data as a hex string. 5928 */ 5929 5930 const hexDump = data => { 5931 const bytes = Array.prototype.slice.call(data); 5932 const step = 16; 5933 let result = ''; 5934 let hex; 5935 let ascii; 5936 5937 for (let j = 0; j < bytes.length / step; j++) { 5938 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 5939 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 5940 result += hex + ' ' + ascii + '\n'; 5941 } 5942 5943 return result; 5944 }; 5945 const tagDump = ({ 5946 bytes 5947 }) => hexDump(bytes); 5948 const textRanges = ranges => { 5949 let result = ''; 5950 let i; 5951 5952 for (i = 0; i < ranges.length; i++) { 5953 result += textRange(ranges, i) + ' '; 5954 } 5955 5956 return result; 5957 }; 5958 5959 var utils = /*#__PURE__*/Object.freeze({ 5960 __proto__: null, 5961 createTransferableMessage: createTransferableMessage, 5962 initSegmentId: initSegmentId, 5963 segmentKeyId: segmentKeyId, 5964 hexDump: hexDump, 5965 tagDump: tagDump, 5966 textRanges: textRanges 5967 }); 5968 5969 // TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux 5970 // 25% was arbitrarily chosen, and may need to be refined over time. 5971 5972 const SEGMENT_END_FUDGE_PERCENT = 0.25; 5973 /** 5974 * Converts a player time (any time that can be gotten/set from player.currentTime(), 5975 * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a 5976 * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)). 5977 * 5978 * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor 5979 * point" (a point where we have a mapping from program time to player time, with player 5980 * time being the post transmux start of the segment). 5981 * 5982 * For more details, see [this doc](../../docs/program-time-from-player-time.md). 5983 * 5984 * @param {number} playerTime the player time 5985 * @param {Object} segment the segment which contains the player time 5986 * @return {Date} program time 5987 */ 5988 5989 const playerTimeToProgramTime = (playerTime, segment) => { 5990 if (!segment.dateTimeObject) { 5991 // Can't convert without an "anchor point" for the program time (i.e., a time that can 5992 // be used to map the start of a segment with a real world time). 5993 return null; 5994 } 5995 5996 const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds; 5997 const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended 5998 5999 const startOfSegment = transmuxedStart + transmuxerPrependedSeconds; 6000 const offsetFromSegmentStart = playerTime - startOfSegment; 6001 return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000); 6002 }; 6003 const originalSegmentVideoDuration = videoTimingInfo => { 6004 return videoTimingInfo.transmuxedPresentationEnd - videoTimingInfo.transmuxedPresentationStart - videoTimingInfo.transmuxerPrependedSeconds; 6005 }; 6006 /** 6007 * Finds a segment that contains the time requested given as an ISO-8601 string. The 6008 * returned segment might be an estimate or an accurate match. 6009 * 6010 * @param {string} programTime The ISO-8601 programTime to find a match for 6011 * @param {Object} playlist A playlist object to search within 6012 */ 6013 6014 const findSegmentForProgramTime = (programTime, playlist) => { 6015 // Assumptions: 6016 // - verifyProgramDateTimeTags has already been run 6017 // - live streams have been started 6018 let dateTimeObject; 6019 6020 try { 6021 dateTimeObject = new Date(programTime); 6022 } catch (e) { 6023 return null; 6024 } 6025 6026 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 6027 return null; 6028 } 6029 6030 let segment = playlist.segments[0]; 6031 6032 if (dateTimeObject < new Date(segment.dateTimeObject)) { 6033 // Requested time is before stream start. 6034 return null; 6035 } 6036 6037 for (let i = 0; i < playlist.segments.length - 1; i++) { 6038 segment = playlist.segments[i]; 6039 const nextSegmentStart = new Date(playlist.segments[i + 1].dateTimeObject); 6040 6041 if (dateTimeObject < nextSegmentStart) { 6042 break; 6043 } 6044 } 6045 6046 const lastSegment = playlist.segments[playlist.segments.length - 1]; 6047 const lastSegmentStart = lastSegment.dateTimeObject; 6048 const lastSegmentDuration = lastSegment.videoTimingInfo ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT; 6049 const lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000); 6050 6051 if (dateTimeObject > lastSegmentEnd) { 6052 // Beyond the end of the stream, or our best guess of the end of the stream. 6053 return null; 6054 } 6055 6056 if (dateTimeObject > new Date(lastSegmentStart)) { 6057 segment = lastSegment; 6058 } 6059 6060 return { 6061 segment, 6062 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)), 6063 // Although, given that all segments have accurate date time objects, the segment 6064 // selected should be accurate, unless the video has been transmuxed at some point 6065 // (determined by the presence of the videoTimingInfo object), the segment's "player 6066 // time" (the start time in the player) can't be considered accurate. 6067 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 6068 }; 6069 }; 6070 /** 6071 * Finds a segment that contains the given player time(in seconds). 6072 * 6073 * @param {number} time The player time to find a match for 6074 * @param {Object} playlist A playlist object to search within 6075 */ 6076 6077 const findSegmentForPlayerTime = (time, playlist) => { 6078 // Assumptions: 6079 // - there will always be a segment.duration 6080 // - we can start from zero 6081 // - segments are in time order 6082 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 6083 return null; 6084 } 6085 6086 let segmentEnd = 0; 6087 let segment; 6088 6089 for (let i = 0; i < playlist.segments.length; i++) { 6090 segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and 6091 // should contain the most accurate values we have for the segment's player times. 6092 // 6093 // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall 6094 // back to an estimate based on the manifest derived (inaccurate) segment.duration, to 6095 // calculate an end value. 6096 6097 segmentEnd = segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration; 6098 6099 if (time <= segmentEnd) { 6100 break; 6101 } 6102 } 6103 6104 const lastSegment = playlist.segments[playlist.segments.length - 1]; 6105 6106 if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) { 6107 // The time requested is beyond the stream end. 6108 return null; 6109 } 6110 6111 if (time > segmentEnd) { 6112 // The time is within or beyond the last segment. 6113 // 6114 // Check to see if the time is beyond a reasonable guess of the end of the stream. 6115 if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) { 6116 // Technically, because the duration value is only an estimate, the time may still 6117 // exist in the last segment, however, there isn't enough information to make even 6118 // a reasonable estimate. 6119 return null; 6120 } 6121 6122 segment = lastSegment; 6123 } 6124 6125 return { 6126 segment, 6127 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration, 6128 // Because videoTimingInfo is only set after transmux, it is the only way to get 6129 // accurate timing values. 6130 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 6131 }; 6132 }; 6133 /** 6134 * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds. 6135 * If the offset returned is positive, the programTime occurs after the 6136 * comparisonTimestamp. 6137 * If the offset is negative, the programTime occurs before the comparisonTimestamp. 6138 * 6139 * @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against 6140 * @param {string} programTime The programTime as an ISO-8601 string 6141 * @return {number} offset 6142 */ 6143 6144 const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => { 6145 let segmentDateTime; 6146 let programDateTime; 6147 6148 try { 6149 segmentDateTime = new Date(comparisonTimeStamp); 6150 programDateTime = new Date(programTime); 6151 } catch (e) {// TODO handle error 6152 } 6153 6154 const segmentTimeEpoch = segmentDateTime.getTime(); 6155 const programTimeEpoch = programDateTime.getTime(); 6156 return (programTimeEpoch - segmentTimeEpoch) / 1000; 6157 }; 6158 /** 6159 * Checks that all segments in this playlist have programDateTime tags. 6160 * 6161 * @param {Object} playlist A playlist object 6162 */ 6163 6164 const verifyProgramDateTimeTags = playlist => { 6165 if (!playlist.segments || playlist.segments.length === 0) { 6166 return false; 6167 } 6168 6169 for (let i = 0; i < playlist.segments.length; i++) { 6170 const segment = playlist.segments[i]; 6171 6172 if (!segment.dateTimeObject) { 6173 return false; 6174 } 6175 } 6176 6177 return true; 6178 }; 6179 /** 6180 * Returns the programTime of the media given a playlist and a playerTime. 6181 * The playlist must have programDateTime tags for a programDateTime tag to be returned. 6182 * If the segments containing the time requested have not been buffered yet, an estimate 6183 * may be returned to the callback. 6184 * 6185 * @param {Object} args 6186 * @param {Object} args.playlist A playlist object to search within 6187 * @param {number} time A playerTime in seconds 6188 * @param {Function} callback(err, programTime) 6189 * @return {string} err.message A detailed error message 6190 * @return {Object} programTime 6191 * @return {number} programTime.mediaSeconds The streamTime in seconds 6192 * @return {string} programTime.programDateTime The programTime as an ISO-8601 String 6193 */ 6194 6195 const getProgramTime = ({ 6196 playlist, 6197 time = undefined, 6198 callback 6199 }) => { 6200 if (!callback) { 6201 throw new Error('getProgramTime: callback must be provided'); 6202 } 6203 6204 if (!playlist || time === undefined) { 6205 return callback({ 6206 message: 'getProgramTime: playlist and time must be provided' 6207 }); 6208 } 6209 6210 const matchedSegment = findSegmentForPlayerTime(time, playlist); 6211 6212 if (!matchedSegment) { 6213 return callback({ 6214 message: 'valid programTime was not found' 6215 }); 6216 } 6217 6218 if (matchedSegment.type === 'estimate') { 6219 return callback({ 6220 message: 'Accurate programTime could not be determined.' + ' Please seek to e.seekTime and try again', 6221 seekTime: matchedSegment.estimatedStart 6222 }); 6223 } 6224 6225 const programTimeObject = { 6226 mediaSeconds: time 6227 }; 6228 const programTime = playerTimeToProgramTime(time, matchedSegment.segment); 6229 6230 if (programTime) { 6231 programTimeObject.programDateTime = programTime.toISOString(); 6232 } 6233 6234 return callback(null, programTimeObject); 6235 }; 6236 /** 6237 * Seeks in the player to a time that matches the given programTime ISO-8601 string. 6238 * 6239 * @param {Object} args 6240 * @param {string} args.programTime A programTime to seek to as an ISO-8601 String 6241 * @param {Object} args.playlist A playlist to look within 6242 * @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2. 6243 * @param {Function} args.seekTo A method to perform a seek 6244 * @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true. 6245 * @param {Object} args.tech The tech to seek on 6246 * @param {Function} args.callback(err, newTime) A callback to return the new time to 6247 * @return {string} err.message A detailed error message 6248 * @return {number} newTime The exact time that was seeked to in seconds 6249 */ 6250 6251 const seekToProgramTime = ({ 6252 programTime, 6253 playlist, 6254 retryCount = 2, 6255 seekTo, 6256 pauseAfterSeek = true, 6257 tech, 6258 callback 6259 }) => { 6260 if (!callback) { 6261 throw new Error('seekToProgramTime: callback must be provided'); 6262 } 6263 6264 if (typeof programTime === 'undefined' || !playlist || !seekTo) { 6265 return callback({ 6266 message: 'seekToProgramTime: programTime, seekTo and playlist must be provided' 6267 }); 6268 } 6269 6270 if (!playlist.endList && !tech.hasStarted_) { 6271 return callback({ 6272 message: 'player must be playing a live stream to start buffering' 6273 }); 6274 } 6275 6276 if (!verifyProgramDateTimeTags(playlist)) { 6277 return callback({ 6278 message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri 6279 }); 6280 } 6281 6282 const matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match 6283 6284 if (!matchedSegment) { 6285 return callback({ 6286 message: `${programTime} was not found in the stream` 6287 }); 6288 } 6289 6290 const segment = matchedSegment.segment; 6291 const mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime); 6292 6293 if (matchedSegment.type === 'estimate') { 6294 // we've run out of retries 6295 if (retryCount === 0) { 6296 return callback({ 6297 message: `${programTime} is not buffered yet. Try again` 6298 }); 6299 } 6300 6301 seekTo(matchedSegment.estimatedStart + mediaOffset); 6302 tech.one('seeked', () => { 6303 seekToProgramTime({ 6304 programTime, 6305 playlist, 6306 retryCount: retryCount - 1, 6307 seekTo, 6308 pauseAfterSeek, 6309 tech, 6310 callback 6311 }); 6312 }); 6313 return; 6314 } // Since the segment.start value is determined from the buffered end or ending time 6315 // of the prior segment, the seekToTime doesn't need to account for any transmuxer 6316 // modifications. 6317 6318 6319 const seekToTime = segment.start + mediaOffset; 6320 6321 const seekedCallback = () => { 6322 return callback(null, tech.currentTime()); 6323 }; // listen for seeked event 6324 6325 6326 tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state 6327 6328 if (pauseAfterSeek) { 6329 tech.pause(); 6330 } 6331 6332 seekTo(seekToTime); 6333 }; 6334 6335 /** 6336 * Loops through all supported media groups in master and calls the provided 6337 * callback for each group 6338 * 6339 * @param {Object} master 6340 * The parsed master manifest object 6341 * @param {string[]} groups 6342 * The media groups to call the callback for 6343 * @param {Function} callback 6344 * Callback to call for each media group 6345 */ 6346 var forEachMediaGroup = function forEachMediaGroup(master, groups, callback) { 6347 groups.forEach(function (mediaType) { 6348 for (var groupKey in master.mediaGroups[mediaType]) { 6349 for (var labelKey in master.mediaGroups[mediaType][groupKey]) { 6350 var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey]; 6351 callback(mediaProperties, mediaType, groupKey, labelKey); 6352 } 6353 } 6354 }); 6355 }; 6356 6357 /*! @name mpd-parser @version 1.
63573.1
vendor: 33,707 bytes, lines 6357-7353
6357 @license Apache-2.0 */ 6358 6359 const isObject = obj => { 6360 return !!obj && typeof obj === 'object'; 6361 }; 6362 6363 const merge = (...objects) => { 6364 return objects.reduce((result, source) => { 6365 if (typeof source !== 'object') { 6366 return result; 6367 } 6368 6369 Object.keys(source).forEach(key => { 6370 if (Array.isArray(result[key]) && Array.isArray(source[key])) { 6371 result[key] = result[key].concat(source[key]); 6372 } else if (isObject(result[key]) && isObject(source[key])) { 6373 result[key] = merge(result[key], source[key]); 6374 } else { 6375 result[key] = source[key]; 6376 } 6377 }); 6378 return result; 6379 }, {}); 6380 }; 6381 6382 const values = o => Object.keys(o).map(k => o[k]); 6383 6384 const range = (start, end) => { 6385 const result = []; 6386 6387 for (let i = start; i < end; i++) { 6388 result.push(i); 6389 } 6390 6391 return result; 6392 }; 6393 6394 const flatten = lists => lists.reduce((x, y) => x.concat(y), []); 6395 6396 const from = list => { 6397 if (!list.length) { 6398 return []; 6399 } 6400 6401 const result = []; 6402 6403 for (let i = 0; i < list.length; i++) { 6404 result.push(list[i]); 6405 } 6406 6407 return result; 6408 }; 6409 6410 const findIndexes = (l, key) => l.reduce((a, e, i) => { 6411 if (e[key]) { 6412 a.push(i); 6413 } 6414 6415 return a; 6416 }, []); 6417 /** 6418 * Returns a union of the included lists provided each element can be identified by a key. 6419 * 6420 * @param {Array} list - list of lists to get the union of 6421 * @param {Function} keyFunction - the function to use as a key for each element 6422 * 6423 * @return {Array} the union of the arrays 6424 */ 6425 6426 6427 const union = (lists, keyFunction) => { 6428 return values(lists.reduce((acc, list) => { 6429 list.forEach(el => { 6430 acc[keyFunction(el)] = el; 6431 }); 6432 return acc; 6433 }, {})); 6434 }; 6435 6436 var errors = { 6437 INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', 6438 INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING', 6439 DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', 6440 DASH_INVALID_XML: 'DASH_INVALID_XML', 6441 NO_BASE_URL: 'NO_BASE_URL', 6442 MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', 6443 SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', 6444 UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' 6445 }; 6446 /** 6447 * @typedef {Object} SingleUri 6448 * @property {string} uri - relative location of segment 6449 * @property {string} resolvedUri - resolved location of segment 6450 * @property {Object} byterange - Object containing information on how to make byte range 6451 * requests following byte-range-spec per RFC2616. 6452 * @property {String} byterange.length - length of range request 6453 * @property {String} byterange.offset - byte offset of range request 6454 * 6455 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 6456 */ 6457 6458 /** 6459 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object 6460 * that conforms to how m3u8-parser is structured 6461 * 6462 * @see https://github.com/videojs/m3u8-parser 6463 * 6464 * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes 6465 * @param {string} source - source url for segment 6466 * @param {string} range - optional range used for range calls, 6467 * follows RFC 2616, Clause 14.35.1 6468 * @return {SingleUri} full segment information transformed into a format similar 6469 * to m3u8-parser 6470 */ 6471 6472 const urlTypeToSegment = ({ 6473 baseUrl = '', 6474 source = '', 6475 range = '', 6476 indexRange = '' 6477 }) => { 6478 const segment = { 6479 uri: source, 6480 resolvedUri: resolveUrl$1(baseUrl || '', source) 6481 }; 6482 6483 if (range || indexRange) { 6484 const rangeStr = range ? range : indexRange; 6485 const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible 6486 6487 let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10); 6488 let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER 6489 6490 if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') { 6491 startRange = Number(startRange); 6492 } 6493 6494 if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') { 6495 endRange = Number(endRange); 6496 } 6497 6498 let length; 6499 6500 if (typeof endRange === 'bigint' || typeof startRange === 'bigint') { 6501 length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1); 6502 } else { 6503 length = endRange - startRange + 1; 6504 } 6505 6506 if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) { 6507 length = Number(length); 6508 } // byterange should be inclusive according to 6509 // RFC 2616, Clause 14.35.1 6510 6511 6512 segment.byterange = { 6513 length, 6514 offset: startRange 6515 }; 6516 } 6517 6518 return segment; 6519 }; 6520 6521 const byteRangeToString = byterange => { 6522 // `endRange` is one less than `offset + length` because the HTTP range 6523 // header uses inclusive ranges 6524 let endRange; 6525 6526 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 6527 endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1); 6528 } else { 6529 endRange = byterange.offset + byterange.length - 1; 6530 } 6531 6532 return `${byterange.offset}-${endRange}`; 6533 }; 6534 /** 6535 * parse the end number attribue that can be a string 6536 * number, or undefined. 6537 * 6538 * @param {string|number|undefined} endNumber 6539 * The end number attribute. 6540 * 6541 * @return {number|null} 6542 * The result of parsing the end number. 6543 */ 6544 6545 6546 const parseEndNumber = endNumber => { 6547 if (endNumber && typeof endNumber !== 'number') { 6548 endNumber = parseInt(endNumber, 10); 6549 } 6550 6551 if (isNaN(endNumber)) { 6552 return null; 6553 } 6554 6555 return endNumber; 6556 }; 6557 /** 6558 * Functions for calculating the range of available segments in static and dynamic 6559 * manifests. 6560 */ 6561 6562 6563 const segmentRange = { 6564 /** 6565 * Returns the entire range of available segments for a static MPD 6566 * 6567 * @param {Object} attributes 6568 * Inheritied MPD attributes 6569 * @return {{ start: number, end: number }} 6570 * The start and end numbers for available segments 6571 */ 6572 static(attributes) { 6573 const { 6574 duration, 6575 timescale = 1, 6576 sourceDuration, 6577 periodDuration 6578 } = attributes; 6579 const endNumber = parseEndNumber(attributes.endNumber); 6580 const segmentDuration = duration / timescale; 6581 6582 if (typeof endNumber === 'number') { 6583 return { 6584 start: 0, 6585 end: endNumber 6586 }; 6587 } 6588 6589 if (typeof periodDuration === 'number') { 6590 return { 6591 start: 0, 6592 end: periodDuration / segmentDuration 6593 }; 6594 } 6595 6596 return { 6597 start: 0, 6598 end: sourceDuration / segmentDuration 6599 }; 6600 }, 6601 6602 /** 6603 * Returns the current live window range of available segments for a dynamic MPD 6604 * 6605 * @param {Object} attributes 6606 * Inheritied MPD attributes 6607 * @return {{ start: number, end: number }} 6608 * The start and end numbers for available segments 6609 */ 6610 dynamic(attributes) { 6611 const { 6612 NOW, 6613 clientOffset, 6614 availabilityStartTime, 6615 timescale = 1, 6616 duration, 6617 periodStart = 0, 6618 minimumUpdatePeriod = 0, 6619 timeShiftBufferDepth = Infinity 6620 } = attributes; 6621 const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated 6622 // after retrieving UTC server time. 6623 6624 const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock. 6625 // Convert the period start time to EPOCH. 6626 6627 const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update. 6628 6629 const periodEndWC = now + minimumUpdatePeriod; 6630 const periodDuration = periodEndWC - periodStartWC; 6631 const segmentCount = Math.ceil(periodDuration * timescale / duration); 6632 const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); 6633 const availableEnd = Math.floor((now - periodStartWC) * timescale / duration); 6634 return { 6635 start: Math.max(0, availableStart), 6636 end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd) 6637 }; 6638 } 6639 6640 }; 6641 /** 6642 * Maps a range of numbers to objects with information needed to build the corresponding 6643 * segment list 6644 * 6645 * @name toSegmentsCallback 6646 * @function 6647 * @param {number} number 6648 * Number of the segment 6649 * @param {number} index 6650 * Index of the number in the range list 6651 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} 6652 * Object with segment timing and duration info 6653 */ 6654 6655 /** 6656 * Returns a callback for Array.prototype.map for mapping a range of numbers to 6657 * information needed to build the segment list. 6658 * 6659 * @param {Object} attributes 6660 * Inherited MPD attributes 6661 * @return {toSegmentsCallback} 6662 * Callback map function 6663 */ 6664 6665 const toSegments = attributes => number => { 6666 const { 6667 duration, 6668 timescale = 1, 6669 periodStart, 6670 startNumber = 1 6671 } = attributes; 6672 return { 6673 number: startNumber + number, 6674 duration: duration / timescale, 6675 timeline: periodStart, 6676 time: number * duration 6677 }; 6678 }; 6679 /** 6680 * Returns a list of objects containing segment timing and duration info used for 6681 * building the list of segments. This uses the @duration attribute specified 6682 * in the MPD manifest to derive the range of segments. 6683 * 6684 * @param {Object} attributes 6685 * Inherited MPD attributes 6686 * @return {{number: number, duration: number, time: number, timeline: number}[]} 6687 * List of Objects with segment timing and duration info 6688 */ 6689 6690 6691 const parseByDuration = attributes => { 6692 const { 6693 type, 6694 duration, 6695 timescale = 1, 6696 periodDuration, 6697 sourceDuration 6698 } = attributes; 6699 const { 6700 start, 6701 end 6702 } = segmentRange[type](attributes); 6703 const segments = range(start, end).map(toSegments(attributes)); 6704 6705 if (type === 'static') { 6706 const index = segments.length - 1; // section is either a period or the full source 6707 6708 const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration 6709 6710 segments[index].duration = sectionDuration - duration / timescale * index; 6711 } 6712 6713 return segments; 6714 }; 6715 /** 6716 * Translates SegmentBase into a set of segments. 6717 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 6718 * node should be translated into segment. 6719 * 6720 * @param {Object} attributes 6721 * Object containing all inherited attributes from parent elements with attribute 6722 * names as keys 6723 * @return {Object.<Array>} list of segments 6724 */ 6725 6726 6727 const segmentsFromBase = attributes => { 6728 const { 6729 baseUrl, 6730 initialization = {}, 6731 sourceDuration, 6732 indexRange = '', 6733 periodStart, 6734 presentationTime, 6735 number = 0, 6736 duration 6737 } = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) 6738 6739 if (!baseUrl) { 6740 throw new Error(errors.NO_BASE_URL); 6741 } 6742 6743 const initSegment = urlTypeToSegment({ 6744 baseUrl, 6745 source: initialization.sourceURL, 6746 range: initialization.range 6747 }); 6748 const segment = urlTypeToSegment({ 6749 baseUrl, 6750 source: baseUrl, 6751 indexRange 6752 }); 6753 segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source 6754 // (since SegmentBase is only for one total segment) 6755 6756 if (duration) { 6757 const segmentTimeInfo = parseByDuration(attributes); 6758 6759 if (segmentTimeInfo.length) { 6760 segment.duration = segmentTimeInfo[0].duration; 6761 segment.timeline = segmentTimeInfo[0].timeline; 6762 } 6763 } else if (sourceDuration) { 6764 segment.duration = sourceDuration; 6765 segment.timeline = periodStart; 6766 } // If presentation time is provided, these segments are being generated by SIDX 6767 // references, and should use the time provided. For the general case of SegmentBase, 6768 // there should only be one segment in the period, so its presentation time is the same 6769 // as its period start. 6770 6771 6772 segment.presentationTime = presentationTime || periodStart; 6773 segment.number = number; 6774 return [segment]; 6775 }; 6776 /** 6777 * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist 6778 * according to the sidx information given. 6779 * 6780 * playlist.sidx has metadadata about the sidx where-as the sidx param 6781 * is the parsed sidx box itself. 6782 * 6783 * @param {Object} playlist the playlist to update the sidx information for 6784 * @param {Object} sidx the parsed sidx box 6785 * @return {Object} the playlist object with the updated sidx information 6786 */ 6787 6788 6789 const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => { 6790 // Retain init segment information 6791 const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing 6792 6793 const sourceDuration = playlist.sidx.duration; // Retain source timeline 6794 6795 const timeline = playlist.timeline || 0; 6796 const sidxByteRange = playlist.sidx.byterange; 6797 const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx 6798 6799 const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes 6800 6801 const mediaReferences = sidx.references.filter(r => r.referenceType !== 1); 6802 const segments = []; 6803 const type = playlist.endList ? 'static' : 'dynamic'; 6804 const periodStart = playlist.sidx.timeline; 6805 let presentationTime = periodStart; 6806 let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box 6807 6808 let startIndex; // eslint-disable-next-line 6809 6810 if (typeof sidx.firstOffset === 'bigint') { 6811 startIndex = window.BigInt(sidxEnd) + sidx.firstOffset; 6812 } else { 6813 startIndex = sidxEnd + sidx.firstOffset; 6814 } 6815 6816 for (let i = 0; i < mediaReferences.length; i++) { 6817 const reference = sidx.references[i]; // size of the referenced (sub)segment 6818 6819 const size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale 6820 // this will be converted to seconds when generating segments 6821 6822 const duration = reference.subsegmentDuration; // should be an inclusive range 6823 6824 let endIndex; // eslint-disable-next-line 6825 6826 if (typeof startIndex === 'bigint') { 6827 endIndex = startIndex + window.BigInt(size) - window.BigInt(1); 6828 } else { 6829 endIndex = startIndex + size - 1; 6830 } 6831 6832 const indexRange = `${startIndex}-${endIndex}`; 6833 const attributes = { 6834 baseUrl, 6835 timescale, 6836 timeline, 6837 periodStart, 6838 presentationTime, 6839 number, 6840 duration, 6841 sourceDuration, 6842 indexRange, 6843 type 6844 }; 6845 const segment = segmentsFromBase(attributes)[0]; 6846 6847 if (initSegment) { 6848 segment.map = initSegment; 6849 } 6850 6851 segments.push(segment); 6852 6853 if (typeof startIndex === 'bigint') { 6854 startIndex += window.BigInt(size); 6855 } else { 6856 startIndex += size; 6857 } 6858 6859 presentationTime += duration / timescale; 6860 number++; 6861 } 6862 6863 playlist.segments = segments; 6864 return playlist; 6865 }; 6866 6867 const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen) 6868 6869 const TIME_FUDGE = 1 / 60; 6870 /** 6871 * Given a list of timelineStarts, combines, dedupes, and sorts them. 6872 * 6873 * @param {TimelineStart[]} timelineStarts - list of timeline starts 6874 * 6875 * @return {TimelineStart[]} the combined and deduped timeline starts 6876 */ 6877 6878 const getUniqueTimelineStarts = timelineStarts => { 6879 return union(timelineStarts, ({ 6880 timeline 6881 }) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1); 6882 }; 6883 /** 6884 * Finds the playlist with the matching NAME attribute. 6885 * 6886 * @param {Array} playlists - playlists to search through 6887 * @param {string} name - the NAME attribute to search for 6888 * 6889 * @return {Object|null} the matching playlist object, or null 6890 */ 6891 6892 6893 const findPlaylistWithName = (playlists, name) => { 6894 for (let i = 0; i < playlists.length; i++) { 6895 if (playlists[i].attributes.NAME === name) { 6896 return playlists[i]; 6897 } 6898 } 6899 6900 return null; 6901 }; 6902 /** 6903 * Gets a flattened array of media group playlists. 6904 * 6905 * @param {Object} manifest - the main manifest object 6906 * 6907 * @return {Array} the media group playlists 6908 */ 6909 6910 6911 const getMediaGroupPlaylists = manifest => { 6912 let mediaGroupPlaylists = []; 6913 forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => { 6914 mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []); 6915 }); 6916 return mediaGroupPlaylists; 6917 }; 6918 /** 6919 * Updates the playlist's media sequence numbers. 6920 * 6921 * @param {Object} config - options object 6922 * @param {Object} config.playlist - the playlist to update 6923 * @param {number} config.mediaSequence - the mediaSequence number to start with 6924 */ 6925 6926 6927 const updateMediaSequenceForPlaylist = ({ 6928 playlist, 6929 mediaSequence 6930 }) => { 6931 playlist.mediaSequence = mediaSequence; 6932 playlist.segments.forEach((segment, index) => { 6933 segment.number = playlist.mediaSequence + index; 6934 }); 6935 }; 6936 /** 6937 * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists 6938 * and a complete list of timeline starts. 6939 * 6940 * If no matching playlist is found, only the discontinuity sequence number of the playlist 6941 * will be updated. 6942 * 6943 * Since early available timelines are not supported, at least one segment must be present. 6944 * 6945 * @param {Object} config - options object 6946 * @param {Object[]} oldPlaylists - the old playlists to use as a reference 6947 * @param {Object[]} newPlaylists - the new playlists to update 6948 * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point 6949 */ 6950 6951 6952 const updateSequenceNumbers = ({ 6953 oldPlaylists, 6954 newPlaylists, 6955 timelineStarts 6956 }) => { 6957 newPlaylists.forEach(playlist => { 6958 playlist.discontinuitySequence = timelineStarts.findIndex(function ({ 6959 timeline 6960 }) { 6961 return timeline === playlist.timeline; 6962 }); // Playlists NAMEs come from DASH Representation IDs, which are mandatory 6963 // (see ISO_23009-1-2012 5.3.5.2). 6964 // 6965 // If the same Representation existed in a prior Period, it will retain the same NAME. 6966 6967 const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME); 6968 6969 if (!oldPlaylist) { 6970 // Since this is a new playlist, the media sequence values can start from 0 without 6971 // consequence. 6972 return; 6973 } // TODO better support for live SIDX 6974 // 6975 // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD). 6976 // This is evident by a playlist only having a single SIDX reference. In a multiperiod 6977 // playlist there would need to be multiple SIDX references. In addition, live SIDX is 6978 // not supported when the SIDX properties change on refreshes. 6979 // 6980 // In the future, if support needs to be added, the merging logic here can be called 6981 // after SIDX references are resolved. For now, exit early to prevent exceptions being 6982 // thrown due to undefined references. 6983 6984 6985 if (playlist.sidx) { 6986 return; 6987 } // Since we don't yet support early available timelines, we don't need to support 6988 // playlists with no segments. 6989 6990 6991 const firstNewSegment = playlist.segments[0]; 6992 const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) { 6993 return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE; 6994 }); // No matching segment from the old playlist means the entire playlist was refreshed. 6995 // In this case the media sequence should account for this update, and the new segments 6996 // should be marked as discontinuous from the prior content, since the last prior 6997 // timeline was removed. 6998 6999 if (oldMatchingSegmentIndex === -1) { 7000 updateMediaSequenceForPlaylist({ 7001 playlist, 7002 mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length 7003 }); 7004 playlist.segments[0].discontinuity = true; 7005 playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content. 7006 // 7007 // If the new playlist's timeline is the same as the last seen segment's timeline, 7008 // then a discontinuity can be added to identify that there's potentially missing 7009 // content. If there's no missing content, the discontinuity should still be rather 7010 // harmless. It's possible that if segment durations are accurate enough, that the 7011 // existence of a gap can be determined using the presentation times and durations, 7012 // but if the segment timing info is off, it may introduce more problems than simply 7013 // adding the discontinuity. 7014 // 7015 // If the new playlist's timeline is different from the last seen segment's timeline, 7016 // then a discontinuity can be added to identify that this is the first seen segment 7017 // of a new timeline. However, the logic at the start of this function that 7018 // determined the disconinuity sequence by timeline index is now off by one (the 7019 // discontinuity of the newest timeline hasn't yet fallen off the manifest...since 7020 // we added it), so the disconinuity sequence must be decremented. 7021 // 7022 // A period may also have a duration of zero, so the case of no segments is handled 7023 // here even though we don't yet support early available periods. 7024 7025 if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) { 7026 playlist.discontinuitySequence--; 7027 } 7028 7029 return; 7030 } // If the first segment matched with a prior segment on a discontinuity (it's matching 7031 // on the first segment of a period), then the discontinuitySequence shouldn't be the 7032 // timeline's matching one, but instead should be the one prior, and the first segment 7033 // of the new manifest should be marked with a discontinuity. 7034 // 7035 // The reason for this special case is that discontinuity sequence shows how many 7036 // discontinuities have fallen off of the playlist, and discontinuities are marked on 7037 // the first segment of a new "timeline." Because of this, while DASH will retain that 7038 // Period while the "timeline" exists, HLS keeps track of it via the discontinuity 7039 // sequence, and that first segment is an indicator, but can be removed before that 7040 // timeline is gone. 7041 7042 7043 const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex]; 7044 7045 if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) { 7046 firstNewSegment.discontinuity = true; 7047 playlist.discontinuityStarts.unshift(0); 7048 playlist.discontinuitySequence--; 7049 } 7050 7051 updateMediaSequenceForPlaylist({ 7052 playlist, 7053 mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number 7054 }); 7055 }); 7056 }; 7057 /** 7058 * Given an old parsed manifest object and a new parsed manifest object, updates the 7059 * sequence and timing values within the new manifest to ensure that it lines up with the 7060 * old. 7061 * 7062 * @param {Array} oldManifest - the old main manifest object 7063 * @param {Array} newManifest - the new main manifest object 7064 * 7065 * @return {Object} the updated new manifest object 7066 */ 7067 7068 7069 const positionManifestOnTimeline = ({ 7070 oldManifest, 7071 newManifest 7072 }) => { 7073 // Starting from v4.1.2 of the IOP, section 4.4.3.3 states: 7074 // 7075 // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates." 7076 // 7077 // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160 7078 // 7079 // Because of this change, and the difficulty of supporting periods with changing start 7080 // times, periods with changing start times are not supported. This makes the logic much 7081 // simpler, since periods with the same start time can be considerred the same period 7082 // across refreshes. 7083 // 7084 // To give an example as to the difficulty of handling periods where the start time may 7085 // change, if a single period manifest is refreshed with another manifest with a single 7086 // period, and both the start and end times are increased, then the only way to determine 7087 // if it's a new period or an old one that has changed is to look through the segments of 7088 // each playlist and determine the presentation time bounds to find a match. In addition, 7089 // if the period start changed to exceed the old period end, then there would be no 7090 // match, and it would not be possible to determine whether the refreshed period is a new 7091 // one or the old one. 7092 const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest)); 7093 const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that 7094 // there's a "memory leak" in that it will never stop growing, in reality, only a couple 7095 // of properties are saved for each seen Period. Even long running live streams won't 7096 // generate too many Periods, unless the stream is watched for decades. In the future, 7097 // this can be optimized by mapping to discontinuity sequence numbers for each timeline, 7098 // but it may not become an issue, and the additional info can be useful for debugging. 7099 7100 newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]); 7101 updateSequenceNumbers({ 7102 oldPlaylists, 7103 newPlaylists, 7104 timelineStarts: newManifest.timelineStarts 7105 }); 7106 return newManifest; 7107 }; 7108 7109 const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange); 7110 7111 const mergeDiscontiguousPlaylists = playlists => { 7112 // Break out playlists into groups based on their baseUrl 7113 const playlistsByBaseUrl = playlists.reduce(function (acc, cur) { 7114 if (!acc[cur.attributes.baseUrl]) { 7115 acc[cur.attributes.baseUrl] = []; 7116 } 7117 7118 acc[cur.attributes.baseUrl].push(cur); 7119 return acc; 7120 }, {}); 7121 let allPlaylists = []; 7122 Object.values(playlistsByBaseUrl).forEach(playlistGroup => { 7123 const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => { 7124 // assuming playlist IDs are the same across periods 7125 // TODO: handle multiperiod where representation sets are not the same 7126 // across periods 7127 const name = playlist.attributes.id + (playlist.attributes.lang || ''); 7128 7129 if (!acc[name]) { 7130 // First Period 7131 acc[name] = playlist; 7132 acc[name].attributes.timelineStarts = []; 7133 } else { 7134 // Subsequent Periods 7135 if (playlist.segments) { 7136 // first segment of subsequent periods signal a discontinuity 7137 if (playlist.segments[0]) { 7138 playlist.segments[0].discontinuity = true; 7139 } 7140 7141 acc[name].segments.push(...playlist.segments); 7142 } // bubble up contentProtection, this assumes all DRM content 7143 // has the same contentProtection 7144 7145 7146 if (playlist.attributes.contentProtection) { 7147 acc[name].attributes.contentProtection = playlist.attributes.contentProtection; 7148 } 7149 } 7150 7151 acc[name].attributes.timelineStarts.push({ 7152 // Although they represent the same number, it's important to have both to make it 7153 // compatible with HLS potentially having a similar attribute. 7154 start: playlist.attributes.periodStart, 7155 timeline: playlist.attributes.periodStart 7156 }); 7157 return acc; 7158 }, {})); 7159 allPlaylists = allPlaylists.concat(mergedPlaylists); 7160 }); 7161 return allPlaylists.map(playlist => { 7162 playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity'); 7163 return playlist; 7164 }); 7165 }; 7166 7167 const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => { 7168 const sidxKey = generateSidxKey(playlist.sidx); 7169 const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx; 7170 7171 if (sidxMatch) { 7172 addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri); 7173 } 7174 7175 return playlist; 7176 }; 7177 7178 const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => { 7179 if (!Object.keys(sidxMapping).length) { 7180 return playlists; 7181 } 7182 7183 for (const i in playlists) { 7184 playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping); 7185 } 7186 7187 return playlists; 7188 }; 7189 7190 const formatAudioPlaylist = ({ 7191 attributes, 7192 segments, 7193 sidx, 7194 mediaSequence, 7195 discontinuitySequence, 7196 discontinuityStarts 7197 }, isAudioOnly) => { 7198 const playlist = { 7199 attributes: { 7200 NAME: attributes.id, 7201 BANDWIDTH: attributes.bandwidth, 7202 CODECS: attributes.codecs, 7203 ['PROGRAM-ID']: 1 7204 }, 7205 uri: '', 7206 endList: attributes.type === 'static', 7207 timeline: attributes.periodStart, 7208 resolvedUri: attributes.baseUrl || '', 7209 targetDuration: attributes.duration, 7210 discontinuitySequence, 7211 discontinuityStarts, 7212 timelineStarts: attributes.timelineStarts, 7213 mediaSequence, 7214 segments 7215 }; 7216 7217 if (attributes.contentProtection) { 7218 playlist.contentProtection = attributes.contentProtection; 7219 } 7220 7221 if (attributes.serviceLocation) { 7222 playlist.attributes.serviceLocation = attributes.serviceLocation; 7223 } 7224 7225 if (sidx) { 7226 playlist.sidx = sidx; 7227 } 7228 7229 if (isAudioOnly) { 7230 playlist.attributes.AUDIO = 'audio'; 7231 playlist.attributes.SUBTITLES = 'subs'; 7232 } 7233 7234 return playlist; 7235 }; 7236 7237 const formatVttPlaylist = ({ 7238 attributes, 7239 segments, 7240 mediaSequence, 7241 discontinuityStarts, 7242 discontinuitySequence 7243 }) => { 7244 if (typeof segments === 'undefined') { 7245 // vtt tracks may use single file in BaseURL 7246 segments = [{ 7247 uri: attributes.baseUrl, 7248 timeline: attributes.periodStart, 7249 resolvedUri: attributes.baseUrl || '', 7250 duration: attributes.sourceDuration, 7251 number: 0 7252 }]; // targetDuration should be the same duration as the only segment 7253 7254 attributes.duration = attributes.sourceDuration; 7255 } 7256 7257 const m3u8Attributes = { 7258 NAME: attributes.id, 7259 BANDWIDTH: attributes.bandwidth, 7260 ['PROGRAM-ID']: 1 7261 }; 7262 7263 if (attributes.codecs) { 7264 m3u8Attributes.CODECS = attributes.codecs; 7265 } 7266 7267 const vttPlaylist = { 7268 attributes: m3u8Attributes, 7269 uri: '', 7270 endList: attributes.type === 'static', 7271 timeline: attributes.periodStart, 7272 resolvedUri: attributes.baseUrl || '', 7273 targetDuration: attributes.duration, 7274 timelineStarts: attributes.timelineStarts, 7275 discontinuityStarts, 7276 discontinuitySequence, 7277 mediaSequence, 7278 segments 7279 }; 7280 7281 if (attributes.serviceLocation) { 7282 vttPlaylist.attributes.serviceLocation = attributes.serviceLocation; 7283 } 7284 7285 return vttPlaylist; 7286 }; 7287 7288 const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => { 7289 let mainPlaylist; 7290 const formattedPlaylists = playlists.reduce((a, playlist) => { 7291 const role = playlist.attributes.role && playlist.attributes.role.value || ''; 7292 const language = playlist.attributes.lang || ''; 7293 let label = playlist.attributes.label || 'main'; 7294 7295 if (language && !playlist.attributes.label) { 7296 const roleLabel = role ? ` (${role})` : ''; 7297 label = `${playlist.attributes.lang}${roleLabel}`; 7298 } 7299 7300 if (!a[label]) { 7301 a[label] = { 7302 language, 7303 autoselect: true, 7304 default: role === 'main', 7305 playlists: [], 7306 uri: '' 7307 }; 7308 } 7309 7310 const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping); 7311 a[label].playlists.push(formatted); 7312 7313 if (typeof mainPlaylist === 'undefined' && role === 'main') { 7314 mainPlaylist = playlist; 7315 mainPlaylist.default = true; 7316 } 7317 7318 return a; 7319 }, {}); // if no playlists have role "main", mark the first as main 7320 7321 if (!mainPlaylist) { 7322 const firstLabel = Object.keys(formattedPlaylists)[0]; 7323 formattedPlaylists[firstLabel].default = true; 7324 } 7325 7326 return formattedPlaylists; 7327 }; 7328 7329 const organizeVttPlaylists = (playlists, sidxMapping = {}) => { 7330 return playlists.reduce((a, playlist) => { 7331 const label = playlist.attributes.label || playlist.attributes.lang || 'text'; 7332 const language = playlist.attributes.lang || 'und'; 7333 7334 if (!a[label]) { 7335 a[label] = { 7336 language, 7337 default: false, 7338 autoselect: false, 7339 playlists: [], 7340 uri: '' 7341 }; 7342 } 7343 7344 a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping)); 7345 return a; 7346 }, {}); 7347 }; 7348 7349 const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => { 7350 if (!svc) { 7351 return svcObj; 7352 } 7353
vendor: 3,850 bytes, lines 7354-7473
7354 svc.forEach(service => { 7355 const { 7356 channel, 7357 language 7358 } = service; 7359 svcObj[language] = { 7360 autoselect: false, 7361 default: false, 7362 instreamId: channel, 7363 language 7364 }; 7365 7366 if (service.hasOwnProperty('aspectRatio')) { 7367 svcObj[language].aspectRatio = service.aspectRatio; 7368 } 7369 7370 if (service.hasOwnProperty('easyReader')) { 7371 svcObj[language].easyReader = service.easyReader; 7372 } 7373 7374 if (service.hasOwnProperty('3D')) { 7375 svcObj[language]['3D'] = service['3D']; 7376 } 7377 }); 7378 return svcObj; 7379 }, {}); 7380 7381 const formatVideoPlaylist = ({ 7382 attributes, 7383 segments, 7384 sidx, 7385 discontinuityStarts 7386 }) => { 7387 const playlist = { 7388 attributes: { 7389 NAME: attributes.id, 7390 AUDIO: 'audio', 7391 SUBTITLES: 'subs', 7392 RESOLUTION: { 7393 width: attributes.width, 7394 height: attributes.height 7395 }, 7396 CODECS: attributes.codecs, 7397 BANDWIDTH: attributes.bandwidth, 7398 ['PROGRAM-ID']: 1 7399 }, 7400 uri: '', 7401 endList: attributes.type === 'static', 7402 timeline: attributes.periodStart, 7403 resolvedUri: attributes.baseUrl || '', 7404 targetDuration: attributes.duration, 7405 discontinuityStarts, 7406 timelineStarts: attributes.timelineStarts, 7407 segments 7408 }; 7409 7410 if (attributes.frameRate) { 7411 playlist.attributes['FRAME-RATE'] = attributes.frameRate; 7412 } 7413 7414 if (attributes.contentProtection) { 7415 playlist.contentProtection = attributes.contentProtection; 7416 } 7417 7418 if (attributes.serviceLocation) { 7419 playlist.attributes.serviceLocation = attributes.serviceLocation; 7420 } 7421 7422 if (sidx) { 7423 playlist.sidx = sidx; 7424 } 7425 7426 return playlist; 7427 }; 7428 7429 const videoOnly = ({ 7430 attributes 7431 }) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video'; 7432 7433 const audioOnly = ({ 7434 attributes 7435 }) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio'; 7436 7437 const vttOnly = ({ 7438 attributes 7439 }) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; 7440 /** 7441 * Contains start and timeline properties denoting a timeline start. For DASH, these will 7442 * be the same number. 7443 * 7444 * @typedef {Object} TimelineStart 7445 * @property {number} start - the start time of the timeline 7446 * @property {number} timeline - the timeline number 7447 */ 7448 7449 /** 7450 * Adds appropriate media and discontinuity sequence values to the segments and playlists. 7451 * 7452 * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a 7453 * DASH specific attribute used in constructing segment URI's from templates. However, from 7454 * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence` 7455 * value, which should start at the original media sequence value (or 0) and increment by 1 7456 * for each segment thereafter. Since DASH's `startNumber` values are independent per 7457 * period, it doesn't make sense to use it for `number`. Instead, assume everything starts 7458 * from a 0 mediaSequence value and increment from there. 7459 * 7460 * Note that VHS currently doesn't use the `number` property, but it can be helpful for 7461 * debugging and making sense of the manifest. 7462 * 7463 * For live playlists, to account for values increasing in manifests when periods are 7464 * removed on refreshes, merging logic should be used to update the numbers to their 7465 * appropriate values (to ensure they're sequential and increasing). 7466 * 7467 * @param {Object[]} playlists - the playlists to update 7468 * @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest 7469 */ 7470 7471 7472 const addMediaSequenceValues = (playlists, timelineStarts) => { 7473 // increment all segments sequentially
vendor: 27,795 bytes, lines 7474-8362
7474 playlists.forEach(playlist => { 7475 playlist.mediaSequence = 0; 7476 playlist.discontinuitySequence = timelineStarts.findIndex(function ({ 7477 timeline 7478 }) { 7479 return timeline === playlist.timeline; 7480 }); 7481 7482 if (!playlist.segments) { 7483 return; 7484 } 7485 7486 playlist.segments.forEach((segment, index) => { 7487 segment.number = index; 7488 }); 7489 }); 7490 }; 7491 /** 7492 * Given a media group object, flattens all playlists within the media group into a single 7493 * array. 7494 * 7495 * @param {Object} mediaGroupObject - the media group object 7496 * 7497 * @return {Object[]} 7498 * The media group playlists 7499 */ 7500 7501 7502 const flattenMediaGroupPlaylists = mediaGroupObject => { 7503 if (!mediaGroupObject) { 7504 return []; 7505 } 7506 7507 return Object.keys(mediaGroupObject).reduce((acc, label) => { 7508 const labelContents = mediaGroupObject[label]; 7509 return acc.concat(labelContents.playlists); 7510 }, []); 7511 }; 7512 7513 const toM3u8 = ({ 7514 dashPlaylists, 7515 locations, 7516 contentSteering, 7517 sidxMapping = {}, 7518 previousManifest, 7519 eventStream 7520 }) => { 7521 if (!dashPlaylists.length) { 7522 return {}; 7523 } // grab all main manifest attributes 7524 7525 7526 const { 7527 sourceDuration: duration, 7528 type, 7529 suggestedPresentationDelay, 7530 minimumUpdatePeriod 7531 } = dashPlaylists[0].attributes; 7532 const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist); 7533 const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly)); 7534 const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly)); 7535 const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean); 7536 const manifest = { 7537 allowCache: true, 7538 discontinuityStarts: [], 7539 segments: [], 7540 endList: true, 7541 mediaGroups: { 7542 AUDIO: {}, 7543 VIDEO: {}, 7544 ['CLOSED-CAPTIONS']: {}, 7545 SUBTITLES: {} 7546 }, 7547 uri: '', 7548 duration, 7549 playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping) 7550 }; 7551 7552 if (minimumUpdatePeriod >= 0) { 7553 manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000; 7554 } 7555 7556 if (locations) { 7557 manifest.locations = locations; 7558 } 7559 7560 if (contentSteering) { 7561 manifest.contentSteering = contentSteering; 7562 } 7563 7564 if (type === 'dynamic') { 7565 manifest.suggestedPresentationDelay = suggestedPresentationDelay; 7566 } 7567 7568 if (eventStream && eventStream.length > 0) { 7569 manifest.eventStream = eventStream; 7570 } 7571 7572 const isAudioOnly = manifest.playlists.length === 0; 7573 const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null; 7574 const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null; 7575 const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup)); 7576 const playlistTimelineStarts = formattedPlaylists.map(({ 7577 timelineStarts 7578 }) => timelineStarts); 7579 manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts); 7580 addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts); 7581 7582 if (organizedAudioGroup) { 7583 manifest.mediaGroups.AUDIO.audio = organizedAudioGroup; 7584 } 7585 7586 if (organizedVttGroup) { 7587 manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup; 7588 } 7589 7590 if (captions.length) { 7591 manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions); 7592 } 7593 7594 if (previousManifest) { 7595 return positionManifestOnTimeline({ 7596 oldManifest: previousManifest, 7597 newManifest: manifest 7598 }); 7599 } 7600 7601 return manifest; 7602 }; 7603 /** 7604 * Calculates the R (repetition) value for a live stream (for the final segment 7605 * in a manifest where the r value is negative 1) 7606 * 7607 * @param {Object} attributes 7608 * Object containing all inherited attributes from parent elements with attribute 7609 * names as keys 7610 * @param {number} time 7611 * current time (typically the total time up until the final segment) 7612 * @param {number} duration 7613 * duration property for the given <S /> 7614 * 7615 * @return {number} 7616 * R value to reach the end of the given period 7617 */ 7618 7619 7620 const getLiveRValue = (attributes, time, duration) => { 7621 const { 7622 NOW, 7623 clientOffset, 7624 availabilityStartTime, 7625 timescale = 1, 7626 periodStart = 0, 7627 minimumUpdatePeriod = 0 7628 } = attributes; 7629 const now = (NOW + clientOffset) / 1000; 7630 const periodStartWC = availabilityStartTime + periodStart; 7631 const periodEndWC = now + minimumUpdatePeriod; 7632 const periodDuration = periodEndWC - periodStartWC; 7633 return Math.ceil((periodDuration * timescale - time) / duration); 7634 }; 7635 /** 7636 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment 7637 * timing and duration 7638 * 7639 * @param {Object} attributes 7640 * Object containing all inherited attributes from parent elements with attribute 7641 * names as keys 7642 * @param {Object[]} segmentTimeline 7643 * List of objects representing the attributes of each S element contained within 7644 * 7645 * @return {{number: number, duration: number, time: number, timeline: number}[]} 7646 * List of Objects with segment timing and duration info 7647 */ 7648 7649 7650 const parseByTimeline = (attributes, segmentTimeline) => { 7651 const { 7652 type, 7653 minimumUpdatePeriod = 0, 7654 media = '', 7655 sourceDuration, 7656 timescale = 1, 7657 startNumber = 1, 7658 periodStart: timeline 7659 } = attributes; 7660 const segments = []; 7661 let time = -1; 7662 7663 for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { 7664 const S = segmentTimeline[sIndex]; 7665 const duration = S.d; 7666 const repeat = S.r || 0; 7667 const segmentTime = S.t || 0; 7668 7669 if (time < 0) { 7670 // first segment 7671 time = segmentTime; 7672 } 7673 7674 if (segmentTime && segmentTime > time) { 7675 // discontinuity 7676 // TODO: How to handle this type of discontinuity 7677 // timeline++ here would treat it like HLS discontuity and content would 7678 // get appended without gap 7679 // E.G. 7680 // <S t="0" d="1" /> 7681 // <S d="1" /> 7682 // <S d="1" /> 7683 // <S t="5" d="1" /> 7684 // would have $Time$ values of [0, 1, 2, 5] 7685 // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) 7686 // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) 7687 // does the value of sourceDuration consider this when calculating arbitrary 7688 // negative @r repeat value? 7689 // E.G. Same elements as above with this added at the end 7690 // <S d="1" r="-1" /> 7691 // with a sourceDuration of 10 7692 // Would the 2 gaps be included in the time duration calculations resulting in 7693 // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments 7694 // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? 7695 time = segmentTime; 7696 } 7697 7698 let count; 7699 7700 if (repeat < 0) { 7701 const nextS = sIndex + 1; 7702 7703 if (nextS === segmentTimeline.length) { 7704 // last segment 7705 if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { 7706 count = getLiveRValue(attributes, time, duration); 7707 } else { 7708 // TODO: This may be incorrect depending on conclusion of TODO above 7709 count = (sourceDuration * timescale - time) / duration; 7710 } 7711 } else { 7712 count = (segmentTimeline[nextS].t - time) / duration; 7713 } 7714 } else { 7715 count = repeat + 1; 7716 } 7717 7718 const end = startNumber + segments.length + count; 7719 let number = startNumber + segments.length; 7720 7721 while (number < end) { 7722 segments.push({ 7723 number, 7724 duration: duration / timescale, 7725 time, 7726 timeline 7727 }); 7728 time += duration; 7729 number++; 7730 } 7731 } 7732 7733 return segments; 7734 }; 7735 7736 const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; 7737 /** 7738 * Replaces template identifiers with corresponding values. To be used as the callback 7739 * for String.prototype.replace 7740 * 7741 * @name replaceCallback 7742 * @function 7743 * @param {string} match 7744 * Entire match of identifier 7745 * @param {string} identifier 7746 * Name of matched identifier 7747 * @param {string} format 7748 * Format tag string. Its presence indicates that padding is expected 7749 * @param {string} width 7750 * Desired length of the replaced value. Values less than this width shall be left 7751 * zero padded 7752 * @return {string} 7753 * Replacement for the matched identifier 7754 */ 7755 7756 /** 7757 * Returns a function to be used as a callback for String.prototype.replace to replace 7758 * template identifiers 7759 * 7760 * @param {Obect} values 7761 * Object containing values that shall be used to replace known identifiers 7762 * @param {number} values.RepresentationID 7763 * Value of the Representation@id attribute 7764 * @param {number} values.Number 7765 * Number of the corresponding segment 7766 * @param {number} values.Bandwidth 7767 * Value of the Representation@bandwidth attribute. 7768 * @param {number} values.Time 7769 * Timestamp value of the corresponding segment 7770 * @return {replaceCallback} 7771 * Callback to be used with String.prototype.replace to replace identifiers 7772 */ 7773 7774 const identifierReplacement = values => (match, identifier, format, width) => { 7775 if (match === '$$') { 7776 // escape sequence 7777 return '$'; 7778 } 7779 7780 if (typeof values[identifier] === 'undefined') { 7781 return match; 7782 } 7783 7784 const value = '' + values[identifier]; 7785 7786 if (identifier === 'RepresentationID') { 7787 // Format tag shall not be present with RepresentationID 7788 return value; 7789 } 7790 7791 if (!format) { 7792 width = 1; 7793 } else { 7794 width = parseInt(width, 10); 7795 } 7796 7797 if (value.length >= width) { 7798 return value; 7799 } 7800 7801 return `${new Array(width - value.length + 1).join('0')}${value}`; 7802 }; 7803 /** 7804 * Constructs a segment url from a template string 7805 * 7806 * @param {string} url 7807 * Template string to construct url from 7808 * @param {Obect} values 7809 * Object containing values that shall be used to replace known identifiers 7810 * @param {number} values.RepresentationID 7811 * Value of the Representation@id attribute 7812 * @param {number} values.Number 7813 * Number of the corresponding segment 7814 * @param {number} values.Bandwidth 7815 * Value of the Representation@bandwidth attribute. 7816 * @param {number} values.Time 7817 * Timestamp value of the corresponding segment 7818 * @return {string} 7819 * Segment url with identifiers replaced 7820 */ 7821 7822 7823 const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values)); 7824 /** 7825 * Generates a list of objects containing timing and duration information about each 7826 * segment needed to generate segment uris and the complete segment object 7827 * 7828 * @param {Object} attributes 7829 * Object containing all inherited attributes from parent elements with attribute 7830 * names as keys 7831 * @param {Object[]|undefined} segmentTimeline 7832 * List of objects representing the attributes of each S element contained within 7833 * the SegmentTimeline element 7834 * @return {{number: number, duration: number, time: number, timeline: number}[]} 7835 * List of Objects with segment timing and duration info 7836 */ 7837 7838 7839 const parseTemplateInfo = (attributes, segmentTimeline) => { 7840 if (!attributes.duration && !segmentTimeline) { 7841 // if neither @duration or SegmentTimeline are present, then there shall be exactly 7842 // one media segment 7843 return [{ 7844 number: attributes.startNumber || 1, 7845 duration: attributes.sourceDuration, 7846 time: 0, 7847 timeline: attributes.periodStart 7848 }]; 7849 } 7850 7851 if (attributes.duration) { 7852 return parseByDuration(attributes); 7853 } 7854 7855 return parseByTimeline(attributes, segmentTimeline); 7856 }; 7857 /** 7858 * Generates a list of segments using information provided by the SegmentTemplate element 7859 * 7860 * @param {Object} attributes 7861 * Object containing all inherited attributes from parent elements with attribute 7862 * names as keys 7863 * @param {Object[]|undefined} segmentTimeline 7864 * List of objects representing the attributes of each S element contained within 7865 * the SegmentTimeline element 7866 * @return {Object[]} 7867 * List of segment objects 7868 */ 7869 7870 7871 const segmentsFromTemplate = (attributes, segmentTimeline) => { 7872 const templateValues = { 7873 RepresentationID: attributes.id, 7874 Bandwidth: attributes.bandwidth || 0 7875 }; 7876 const { 7877 initialization = { 7878 sourceURL: '', 7879 range: '' 7880 } 7881 } = attributes; 7882 const mapSegment = urlTypeToSegment({ 7883 baseUrl: attributes.baseUrl, 7884 source: constructTemplateUrl(initialization.sourceURL, templateValues), 7885 range: initialization.range 7886 }); 7887 const segments = parseTemplateInfo(attributes, segmentTimeline); 7888 return segments.map(segment => { 7889 templateValues.Number = segment.number; 7890 templateValues.Time = segment.time; 7891 const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2 7892 // - if timescale isn't present on any level, default to 1. 7893 7894 const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0 7895 7896 const presentationTimeOffset = attributes.presentationTimeOffset || 0; 7897 const presentationTime = // Even if the @t attribute is not specified for the segment, segment.time is 7898 // calculated in mpd-parser prior to this, so it's assumed to be available. 7899 attributes.periodStart + (segment.time - presentationTimeOffset) / timescale; 7900 const map = { 7901 uri, 7902 timeline: segment.timeline, 7903 duration: segment.duration, 7904 resolvedUri: resolveUrl$1(attributes.baseUrl || '', uri), 7905 map: mapSegment, 7906 number: segment.number, 7907 presentationTime 7908 }; 7909 return map; 7910 }); 7911 }; 7912 /** 7913 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14) 7914 * to an object that matches the output of a segment in videojs/mpd-parser 7915 * 7916 * @param {Object} attributes 7917 * Object containing all inherited attributes from parent elements with attribute 7918 * names as keys 7919 * @param {Object} segmentUrl 7920 * <SegmentURL> node to translate into a segment object 7921 * @return {Object} translated segment object 7922 */ 7923 7924 7925 const SegmentURLToSegmentObject = (attributes, segmentUrl) => { 7926 const { 7927 baseUrl, 7928 initialization = {} 7929 } = attributes; 7930 const initSegment = urlTypeToSegment({ 7931 baseUrl, 7932 source: initialization.sourceURL, 7933 range: initialization.range 7934 }); 7935 const segment = urlTypeToSegment({ 7936 baseUrl, 7937 source: segmentUrl.media, 7938 range: segmentUrl.mediaRange 7939 }); 7940 segment.map = initSegment; 7941 return segment; 7942 }; 7943 /** 7944 * Generates a list of segments using information provided by the SegmentList element 7945 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 7946 * node should be translated into segment. 7947 * 7948 * @param {Object} attributes 7949 * Object containing all inherited attributes from parent elements with attribute 7950 * names as keys 7951 * @param {Object[]|undefined} segmentTimeline 7952 * List of objects representing the attributes of each S element contained within 7953 * the SegmentTimeline element 7954 * @return {Object.<Array>} list of segments 7955 */ 7956 7957 7958 const segmentsFromList = (attributes, segmentTimeline) => { 7959 const { 7960 duration, 7961 segmentUrls = [], 7962 periodStart 7963 } = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR 7964 // if both SegmentTimeline and @duration are defined, it is outside of spec. 7965 7966 if (!duration && !segmentTimeline || duration && segmentTimeline) { 7967 throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); 7968 } 7969 7970 const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject)); 7971 let segmentTimeInfo; 7972 7973 if (duration) { 7974 segmentTimeInfo = parseByDuration(attributes); 7975 } 7976 7977 if (segmentTimeline) { 7978 segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); 7979 } 7980 7981 const segments = segmentTimeInfo.map((segmentTime, index) => { 7982 if (segmentUrlMap[index]) { 7983 const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2 7984 // - if timescale isn't present on any level, default to 1. 7985 7986 const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0 7987 7988 const presentationTimeOffset = attributes.presentationTimeOffset || 0; 7989 segment.timeline = segmentTime.timeline; 7990 segment.duration = segmentTime.duration; 7991 segment.number = segmentTime.number; 7992 segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale; 7993 return segment; 7994 } // Since we're mapping we should get rid of any blank segments (in case 7995 // the given SegmentTimeline is handling for more elements than we have 7996 // SegmentURLs for). 7997 7998 }).filter(segment => segment); 7999 return segments; 8000 }; 8001 8002 const generateSegments = ({ 8003 attributes, 8004 segmentInfo 8005 }) => { 8006 let segmentAttributes; 8007 let segmentsFn; 8008 8009 if (segmentInfo.template) { 8010 segmentsFn = segmentsFromTemplate; 8011 segmentAttributes = merge(attributes, segmentInfo.template); 8012 } else if (segmentInfo.base) { 8013 segmentsFn = segmentsFromBase; 8014 segmentAttributes = merge(attributes, segmentInfo.base); 8015 } else if (segmentInfo.list) { 8016 segmentsFn = segmentsFromList; 8017 segmentAttributes = merge(attributes, segmentInfo.list); 8018 } 8019 8020 const segmentsInfo = { 8021 attributes 8022 }; 8023 8024 if (!segmentsFn) { 8025 return segmentsInfo; 8026 } 8027 8028 const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which 8029 // must be in seconds. Since we've generated the segment list, we no longer need 8030 // @duration to be in @timescale units, so we can convert it here. 8031 8032 if (segmentAttributes.duration) { 8033 const { 8034 duration, 8035 timescale = 1 8036 } = segmentAttributes; 8037 segmentAttributes.duration = duration / timescale; 8038 } else if (segments.length) { 8039 // if there is no @duration attribute, use the largest segment duration as 8040 // as target duration 8041 segmentAttributes.duration = segments.reduce((max, segment) => { 8042 return Math.max(max, Math.ceil(segment.duration)); 8043 }, 0); 8044 } else { 8045 segmentAttributes.duration = 0; 8046 } 8047 8048 segmentsInfo.attributes = segmentAttributes; 8049 segmentsInfo.segments = segments; // This is a sidx box without actual segment information 8050 8051 if (segmentInfo.base && segmentAttributes.indexRange) { 8052 segmentsInfo.sidx = segments[0]; 8053 segmentsInfo.segments = []; 8054 } 8055 8056 return segmentsInfo; 8057 }; 8058 8059 const toPlaylists = representations => representations.map(generateSegments); 8060 8061 const findChildren = (element, name) => from(element.childNodes).filter(({ 8062 tagName 8063 }) => tagName === name); 8064 8065 const getContent = element => element.textContent.trim(); 8066 /** 8067 * Converts the provided string that may contain a division operation to a number. 8068 * 8069 * @param {string} value - the provided string value 8070 * 8071 * @return {number} the parsed string value 8072 */ 8073 8074 8075 const parseDivisionValue = value => { 8076 return parseFloat(value.split('/').reduce((prev, current) => prev / current)); 8077 }; 8078 8079 const parseDuration = str => { 8080 const SECONDS_IN_YEAR = 365 * 24 * 60 * 60; 8081 const SECONDS_IN_MONTH = 30 * 24 * 60 * 60; 8082 const SECONDS_IN_DAY = 24 * 60 * 60; 8083 const SECONDS_IN_HOUR = 60 * 60; 8084 const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S 8085 8086 const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; 8087 const match = durationRegex.exec(str); 8088 8089 if (!match) { 8090 return 0; 8091 } 8092 8093 const [year, month, day, hour, minute, second] = match.slice(1); 8094 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); 8095 }; 8096 8097 const parseDate = str => { 8098 // Date format without timezone according to ISO 8601 8099 // YYY-MM-DDThh:mm:ss.ssssss 8100 const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is 8101 // expressed by ending with 'Z' 8102 8103 if (dateRegex.test(str)) { 8104 str += 'Z'; 8105 } 8106 8107 return Date.parse(str); 8108 }; 8109 8110 const parsers = { 8111 /** 8112 * Specifies the duration of the entire Media Presentation. Format is a duration string 8113 * as specified in ISO 8601 8114 * 8115 * @param {string} value 8116 * value of attribute as a string 8117 * @return {number} 8118 * The duration in seconds 8119 */ 8120 mediaPresentationDuration(value) { 8121 return parseDuration(value); 8122 }, 8123 8124 /** 8125 * Specifies the Segment availability start time for all Segments referred to in this 8126 * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability 8127 * time. Format is a date string as specified in ISO 8601 8128 * 8129 * @param {string} value 8130 * value of attribute as a string 8131 * @return {number} 8132 * The date as seconds from unix epoch 8133 */ 8134 availabilityStartTime(value) { 8135 return parseDate(value) / 1000; 8136 }, 8137 8138 /** 8139 * Specifies the smallest period between potential changes to the MPD. Format is a 8140 * duration string as specified in ISO 8601 8141 * 8142 * @param {string} value 8143 * value of attribute as a string 8144 * @return {number} 8145 * The duration in seconds 8146 */ 8147 minimumUpdatePeriod(value) { 8148 return parseDuration(value); 8149 }, 8150 8151 /** 8152 * Specifies the suggested presentation delay. Format is a 8153 * duration string as specified in ISO 8601 8154 * 8155 * @param {string} value 8156 * value of attribute as a string 8157 * @return {number} 8158 * The duration in seconds 8159 */ 8160 suggestedPresentationDelay(value) { 8161 return parseDuration(value); 8162 }, 8163 8164 /** 8165 * specifices the type of mpd. Can be either "static" or "dynamic" 8166 * 8167 * @param {string} value 8168 * value of attribute as a string 8169 * 8170 * @return {string} 8171 * The type as a string 8172 */ 8173 type(value) { 8174 return value; 8175 }, 8176 8177 /** 8178 * Specifies the duration of the smallest time shifting buffer for any Representation 8179 * in the MPD. Format is a duration string as specified in ISO 8601 8180 * 8181 * @param {string} value 8182 * value of attribute as a string 8183 * @return {number} 8184 * The duration in seconds 8185 */ 8186 timeShiftBufferDepth(value) { 8187 return parseDuration(value); 8188 }, 8189 8190 /** 8191 * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. 8192 * Format is a duration string as specified in ISO 8601 8193 * 8194 * @param {string} value 8195 * value of attribute as a string 8196 * @return {number} 8197 * The duration in seconds 8198 */ 8199 start(value) { 8200 return parseDuration(value); 8201 }, 8202 8203 /** 8204 * Specifies the width of the visual presentation 8205 * 8206 * @param {string} value 8207 * value of attribute as a string 8208 * @return {number} 8209 * The parsed width 8210 */ 8211 width(value) { 8212 return parseInt(value, 10); 8213 }, 8214 8215 /** 8216 * Specifies the height of the visual presentation 8217 * 8218 * @param {string} value 8219 * value of attribute as a string 8220 * @return {number} 8221 * The parsed height 8222 */ 8223 height(value) { 8224 return parseInt(value, 10); 8225 }, 8226 8227 /** 8228 * Specifies the bitrate of the representation 8229 * 8230 * @param {string} value 8231 * value of attribute as a string 8232 * @return {number} 8233 * The parsed bandwidth 8234 */ 8235 bandwidth(value) { 8236 return parseInt(value, 10); 8237 }, 8238 8239 /** 8240 * Specifies the frame rate of the representation 8241 * 8242 * @param {string} value 8243 * value of attribute as a string 8244 * @return {number} 8245 * The parsed frame rate 8246 */ 8247 frameRate(value) { 8248 return parseDivisionValue(value); 8249 }, 8250 8251 /** 8252 * Specifies the number of the first Media Segment in this Representation in the Period 8253 * 8254 * @param {string} value 8255 * value of attribute as a string 8256 * @return {number} 8257 * The parsed number 8258 */ 8259 startNumber(value) { 8260 return parseInt(value, 10); 8261 }, 8262 8263 /** 8264 * Specifies the timescale in units per seconds 8265 * 8266 * @param {string} value 8267 * value of attribute as a string 8268 * @return {number} 8269 * The parsed timescale 8270 */ 8271 timescale(value) { 8272 return parseInt(value, 10); 8273 }, 8274 8275 /** 8276 * Specifies the presentationTimeOffset. 8277 * 8278 * @param {string} value 8279 * value of the attribute as a string 8280 * 8281 * @return {number} 8282 * The parsed presentationTimeOffset 8283 */ 8284 presentationTimeOffset(value) { 8285 return parseInt(value, 10); 8286 }, 8287 8288 /** 8289 * Specifies the constant approximate Segment duration 8290 * NOTE: The <Period> element also contains an @duration attribute. This duration 8291 * specifies the duration of the Period. This attribute is currently not 8292 * supported by the rest of the parser, however we still check for it to prevent 8293 * errors. 8294 * 8295 * @param {string} value 8296 * value of attribute as a string 8297 * @return {number} 8298 * The parsed duration 8299 */ 8300 duration(value) { 8301 const parsedValue = parseInt(value, 10); 8302 8303 if (isNaN(parsedValue)) { 8304 return parseDuration(value); 8305 } 8306 8307 return parsedValue; 8308 }, 8309 8310 /** 8311 * Specifies the Segment duration, in units of the value of the @timescale. 8312 * 8313 * @param {string} value 8314 * value of attribute as a string 8315 * @return {number} 8316 * The parsed duration 8317 */ 8318 d(value) { 8319 return parseInt(value, 10); 8320 }, 8321 8322 /** 8323 * Specifies the MPD start time, in @timescale units, the first Segment in the series 8324 * starts relative to the beginning of the Period 8325 * 8326 * @param {string} value 8327 * value of attribute as a string 8328 * @return {number} 8329 * The parsed time 8330 */ 8331 t(value) { 8332 return parseInt(value, 10); 8333 }, 8334 8335 /** 8336 * Specifies the repeat count of the number of following contiguous Segments with the 8337 * same duration expressed by the value of @d 8338 * 8339 * @param {string} value 8340 * value of attribute as a string 8341 * @return {number} 8342 * The parsed number 8343 */ 8344 r(value) { 8345 return parseInt(value, 10); 8346 }, 8347 8348 /** 8349 * Specifies the presentationTime. 8350 * 8351 * @param {string} value 8352 * value of the attribute as a string 8353 * 8354 * @return {number} 8355 * The parsed presentationTime 8356 */ 8357 presentationTime(value) { 8358 return parseInt(value, 10); 8359 }, 8360 8361 /** 8362 *
8362Default parser for all other attributes. Acts as a no-op and just returns the value 8363 * as a string 8364 * 8365 * @param {string} value 8366 * value of attribute as a string 8367 * @return {string} 8368 * Unparsed value 8369 */ 8370 DEFAULT(value) { 8371 return value; 8372 } 8373 8374 }; 8375 /** 8376 * Gets all the attributes and values of the provided node, parses attributes with known 8377 * types, and returns an object with attribute names mapped to values. 8378 * 8379 * @param {Node} el 8380 * The node to parse attributes from 8381 * @return {Object} 8382 * Object with all attributes of el parsed 8383 */ 8384 8385 const parseAttributes = el => { 8386 if (!(el && el.attributes)) { 8387 return {}; 8388 } 8389 8390 return from(el.attributes).reduce((a, e) => { 8391 const parseFn = parsers[e.name] || parsers.DEFAULT; 8392 a[e.n
vendor: 31,526 bytes, lines 8392-9250
8392ame] = parseFn(e.value); 8393 return a; 8394 }, {}); 8395 }; 8396 8397 const keySystemsMap = { 8398 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', 8399 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', 8400 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', 8401 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime', 8402 // ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme 8403 'urn:mpeg:dash:mp4protection:2011': 'mp4protection' 8404 }; 8405 /** 8406 * Builds a list of urls that is the product of the reference urls and BaseURL values 8407 * 8408 * @param {Object[]} references 8409 * List of objects containing the reference URL as well as its attributes 8410 * @param {Node[]} baseUrlElements 8411 * List of BaseURL nodes from the mpd 8412 * @return {Object[]} 8413 * List of objects with resolved urls and attributes 8414 */ 8415 8416 const buildBaseUrls = (references, baseUrlElements) => { 8417 if (!baseUrlElements.length) { 8418 return references; 8419 } 8420 8421 return flatten(references.map(function (reference) { 8422 return baseUrlElements.map(function (baseUrlElement) { 8423 const initialBaseUrl = getContent(baseUrlElement); 8424 const resolvedBaseUrl = resolveUrl$1(reference.baseUrl, initialBaseUrl); 8425 const finalBaseUrl = merge(parseAttributes(baseUrlElement), { 8426 baseUrl: resolvedBaseUrl 8427 }); // If the URL is resolved, we want to get the serviceLocation from the reference 8428 // assuming there is no serviceLocation on the initialBaseUrl 8429 8430 if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) { 8431 finalBaseUrl.serviceLocation = reference.serviceLocation; 8432 } 8433 8434 return finalBaseUrl; 8435 }); 8436 })); 8437 }; 8438 /** 8439 * Contains all Segment information for its containing AdaptationSet 8440 * 8441 * @typedef {Object} SegmentInformation 8442 * @property {Object|undefined} template 8443 * Contains the attributes for the SegmentTemplate node 8444 * @property {Object[]|undefined} segmentTimeline 8445 * Contains a list of atrributes for each S node within the SegmentTimeline node 8446 * @property {Object|undefined} list 8447 * Contains the attributes for the SegmentList node 8448 * @property {Object|undefined} base 8449 * Contains the attributes for the SegmentBase node 8450 */ 8451 8452 /** 8453 * Returns all available Segment information contained within the AdaptationSet node 8454 * 8455 * @param {Node} adaptationSet 8456 * The AdaptationSet node to get Segment information from 8457 * @return {SegmentInformation} 8458 * The Segment information contained within the provided AdaptationSet 8459 */ 8460 8461 8462 const getSegmentInformation = adaptationSet => { 8463 const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; 8464 const segmentList = findChildren(adaptationSet, 'SegmentList')[0]; 8465 const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge({ 8466 tag: 'SegmentURL' 8467 }, parseAttributes(s))); 8468 const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; 8469 const segmentTimelineParentNode = segmentList || segmentTemplate; 8470 const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; 8471 const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; 8472 const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both 8473 // @initialization and an <Initialization> node. @initialization can be templated, 8474 // while the node can have a url and range specified. If the <SegmentTemplate> has 8475 // both @initialization and an <Initialization> subelement we opt to override with 8476 // the node, as this interaction is not defined in the spec. 8477 8478 const template = segmentTemplate && parseAttributes(segmentTemplate); 8479 8480 if (template && segmentInitialization) { 8481 template.initialization = segmentInitialization && parseAttributes(segmentInitialization); 8482 } else if (template && template.initialization) { 8483 // If it is @initialization we convert it to an object since this is the format that 8484 // later functions will rely on for the initialization segment. This is only valid 8485 // for <SegmentTemplate> 8486 template.initialization = { 8487 sourceURL: template.initialization 8488 }; 8489 } 8490 8491 const segmentInfo = { 8492 template, 8493 segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)), 8494 list: segmentList && merge(parseAttributes(segmentList), { 8495 segmentUrls, 8496 initialization: parseAttributes(segmentInitialization) 8497 }), 8498 base: segmentBase && merge(parseAttributes(segmentBase), { 8499 initialization: parseAttributes(segmentInitialization) 8500 }) 8501 }; 8502 Object.keys(segmentInfo).forEach(key => { 8503 if (!segmentInfo[key]) { 8504 delete segmentInfo[key]; 8505 } 8506 }); 8507 return segmentInfo; 8508 }; 8509 /** 8510 * Contains Segment information and attributes needed to construct a Playlist object 8511 * from a Representation 8512 * 8513 * @typedef {Object} RepresentationInformation 8514 * @property {SegmentInformation} segmentInfo 8515 * Segment information for this Representation 8516 * @property {Object} attributes 8517 * Inherited attributes for this Representation 8518 */ 8519 8520 /** 8521 * Maps a Representation node to an object containing Segment information and attributes 8522 * 8523 * @name inheritBaseUrlsCallback 8524 * @function 8525 * @param {Node} representation 8526 * Representation node from the mpd 8527 * @return {RepresentationInformation} 8528 * Representation information needed to construct a Playlist object 8529 */ 8530 8531 /** 8532 * Returns a callback for Array.prototype.map for mapping Representation nodes to 8533 * Segment information and attributes using inherited BaseURL nodes. 8534 * 8535 * @param {Object} adaptationSetAttributes 8536 * Contains attributes inherited by the AdaptationSet 8537 * @param {Object[]} adaptationSetBaseUrls 8538 * List of objects containing resolved base URLs and attributes 8539 * inherited by the AdaptationSet 8540 * @param {SegmentInformation} adaptationSetSegmentInfo 8541 * Contains Segment information for the AdaptationSet 8542 * @return {inheritBaseUrlsCallback} 8543 * Callback map function 8544 */ 8545 8546 8547 const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => { 8548 const repBaseUrlElements = findChildren(representation, 'BaseURL'); 8549 const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); 8550 const attributes = merge(adaptationSetAttributes, parseAttributes(representation)); 8551 const representationSegmentInfo = getSegmentInformation(representation); 8552 return repBaseUrls.map(baseUrl => { 8553 return { 8554 segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo), 8555 attributes: merge(attributes, baseUrl) 8556 }; 8557 }); 8558 }; 8559 /** 8560 * Tranforms a series of content protection nodes to 8561 * an object containing pssh data by key system 8562 * 8563 * @param {Node[]} contentProtectionNodes 8564 * Content protection nodes 8565 * @return {Object} 8566 * Object containing pssh data by key system 8567 */ 8568 8569 8570 const generateKeySystemInformation = contentProtectionNodes => { 8571 return contentProtectionNodes.reduce((acc, node) => { 8572 const attributes = parseAttributes(node); // Although it could be argued that according to the UUID RFC spec the UUID string (a-f chars) should be generated 8573 // as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system 8574 // UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do 8575 // .toLowerCase() on the input UUID string from the manifest (at least I could not think of one). 8576 8577 if (attributes.schemeIdUri) { 8578 attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase(); 8579 } 8580 8581 const keySystem = keySystemsMap[attributes.schemeIdUri]; 8582 8583 if (keySystem) { 8584 acc[keySystem] = { 8585 attributes 8586 }; 8587 const psshNode = findChildren(node, 'cenc:pssh')[0]; 8588 8589 if (psshNode) { 8590 const pssh = getContent(psshNode); 8591 acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh); 8592 } 8593 } 8594 8595 return acc; 8596 }, {}); 8597 }; // defined in ANSI_SCTE 214-1 2016 8598 8599 8600 const parseCaptionServiceMetadata = service => { 8601 // 608 captions 8602 if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') { 8603 const values = typeof service.value !== 'string' ? [] : service.value.split(';'); 8604 return values.map(value => { 8605 let channel; 8606 let language; // default language to value 8607 8608 language = value; 8609 8610 if (/^CC\d=/.test(value)) { 8611 [channel, language] = value.split('='); 8612 } else if (/^CC\d$/.test(value)) { 8613 channel = value; 8614 } 8615 8616 return { 8617 channel, 8618 language 8619 }; 8620 }); 8621 } else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') { 8622 const values = typeof service.value !== 'string' ? [] : service.value.split(';'); 8623 return values.map(value => { 8624 const flags = { 8625 // service or channel number 1-63 8626 'channel': undefined, 8627 // language is a 3ALPHA per ISO 639.2/B 8628 // field is required 8629 'language': undefined, 8630 // BIT 1/0 or ? 8631 // default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown 8632 'aspectRatio': 1, 8633 // BIT 1/0 8634 // easy reader flag indicated the text is tailed to the needs of beginning readers 8635 // default 0, or off 8636 'easyReader': 0, 8637 // BIT 1/0 8638 // If 3d metadata is present (CEA-708.1) then 1 8639 // default 0 8640 '3D': 0 8641 }; 8642 8643 if (/=/.test(value)) { 8644 const [channel, opts = ''] = value.split('='); 8645 flags.channel = channel; 8646 flags.language = value; 8647 opts.split(',').forEach(opt => { 8648 const [name, val] = opt.split(':'); 8649 8650 if (name === 'lang') { 8651 flags.language = val; // er for easyReadery 8652 } else if (name === 'er') { 8653 flags.easyReader = Number(val); // war for wide aspect ratio 8654 } else if (name === 'war') { 8655 flags.aspectRatio = Number(val); 8656 } else if (name === '3D') { 8657 flags['3D'] = Number(val); 8658 } 8659 }); 8660 } else { 8661 flags.language = value; 8662 } 8663 8664 if (flags.channel) { 8665 flags.channel = 'SERVICE' + flags.channel; 8666 } 8667 8668 return flags; 8669 }); 8670 } 8671 }; 8672 /** 8673 * A map callback that will parse all event stream data for a collection of periods 8674 * DASH ISO_IEC_23009 5.10.2.2 8675 * https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing 8676 * 8677 * @param {PeriodInformation} period object containing necessary period information 8678 * @return a collection of parsed eventstream event objects 8679 */ 8680 8681 8682 const toEventStream = period => { 8683 // get and flatten all EventStreams tags and parse attributes and children 8684 return flatten(findChildren(period.node, 'EventStream').map(eventStream => { 8685 const eventStreamAttributes = parseAttributes(eventStream); 8686 const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects 8687 8688 return findChildren(eventStream, 'Event').map(event => { 8689 const eventAttributes = parseAttributes(event); 8690 const presentationTime = eventAttributes.presentationTime || 0; 8691 const timescale = eventStreamAttributes.timescale || 1; 8692 const duration = eventAttributes.duration || 0; 8693 const start = presentationTime / timescale + period.attributes.start; 8694 return { 8695 schemeIdUri, 8696 value: eventStreamAttributes.value, 8697 id: eventAttributes.id, 8698 start, 8699 end: start + duration / timescale, 8700 messageData: getContent(event) || eventAttributes.messageData, 8701 contentEncoding: eventStreamAttributes.contentEncoding, 8702 presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0 8703 }; 8704 }); 8705 })); 8706 }; 8707 /** 8708 * Maps an AdaptationSet node to a list of Representation information objects 8709 * 8710 * @name toRepresentationsCallback 8711 * @function 8712 * @param {Node} adaptationSet 8713 * AdaptationSet node from the mpd 8714 * @return {RepresentationInformation[]} 8715 * List of objects containing Representaion information 8716 */ 8717 8718 /** 8719 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of 8720 * Representation information objects 8721 * 8722 * @param {Object} periodAttributes 8723 * Contains attributes inherited by the Period 8724 * @param {Object[]} periodBaseUrls 8725 * Contains list of objects with resolved base urls and attributes 8726 * inherited by the Period 8727 * @param {string[]} periodSegmentInfo 8728 * Contains Segment Information at the period level 8729 * @return {toRepresentationsCallback} 8730 * Callback map function 8731 */ 8732 8733 8734 const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => { 8735 const adaptationSetAttributes = parseAttributes(adaptationSet); 8736 const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); 8737 const role = findChildren(adaptationSet, 'Role')[0]; 8738 const roleAttributes = { 8739 role: parseAttributes(role) 8740 }; 8741 let attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes); 8742 const accessibility = findChildren(adaptationSet, 'Accessibility')[0]; 8743 const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility)); 8744 8745 if (captionServices) { 8746 attrs = merge(attrs, { 8747 captionServices 8748 }); 8749 } 8750 8751 const label = findChildren(adaptationSet, 'Label')[0]; 8752 8753 if (label && label.childNodes.length) { 8754 const labelVal = label.childNodes[0].nodeValue.trim(); 8755 attrs = merge(attrs, { 8756 label: labelVal 8757 }); 8758 } 8759 8760 const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); 8761 8762 if (Object.keys(contentProtection).length) { 8763 attrs = merge(attrs, { 8764 contentProtection 8765 }); 8766 } 8767 8768 const segmentInfo = getSegmentInformation(adaptationSet); 8769 const representations = findChildren(adaptationSet, 'Representation'); 8770 const adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo); 8771 return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); 8772 }; 8773 /** 8774 * Contains all period information for mapping nodes onto adaptation sets. 8775 * 8776 * @typedef {Object} PeriodInformation 8777 * @property {Node} period.node 8778 * Period node from the mpd 8779 * @property {Object} period.attributes 8780 * Parsed period attributes from node plus any added 8781 */ 8782 8783 /** 8784 * Maps a PeriodInformation object to a list of Representation information objects for all 8785 * AdaptationSet nodes contained within the Period. 8786 * 8787 * @name toAdaptationSetsCallback 8788 * @function 8789 * @param {PeriodInformation} period 8790 * Period object containing necessary period information 8791 * @param {number} periodStart 8792 * Start time of the Period within the mpd 8793 * @return {RepresentationInformation[]} 8794 * List of objects containing Representaion information 8795 */ 8796 8797 /** 8798 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of 8799 * Representation information objects 8800 * 8801 * @param {Object} mpdAttributes 8802 * Contains attributes inherited by the mpd 8803 * @param {Object[]} mpdBaseUrls 8804 * Contains list of objects with resolved base urls and attributes 8805 * inherited by the mpd 8806 * @return {toAdaptationSetsCallback} 8807 * Callback map function 8808 */ 8809 8810 8811 const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => { 8812 const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL')); 8813 const periodAttributes = merge(mpdAttributes, { 8814 periodStart: period.attributes.start 8815 }); 8816 8817 if (typeof period.attributes.duration === 'number') { 8818 periodAttributes.periodDuration = period.attributes.duration; 8819 } 8820 8821 const adaptationSets = findChildren(period.node, 'AdaptationSet'); 8822 const periodSegmentInfo = getSegmentInformation(period.node); 8823 return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); 8824 }; 8825 /** 8826 * Tranforms an array of content steering nodes into an object 8827 * containing CDN content steering information from the MPD manifest. 8828 * 8829 * For more information on the DASH spec for Content Steering parsing, see: 8830 * https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 8831 * 8832 * @param {Node[]} contentSteeringNodes 8833 * Content steering nodes 8834 * @param {Function} eventHandler 8835 * The event handler passed into the parser options to handle warnings 8836 * @return {Object} 8837 * Object containing content steering data 8838 */ 8839 8840 8841 const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => { 8842 // If there are more than one ContentSteering tags, throw an error 8843 if (contentSteeringNodes.length > 1) { 8844 eventHandler({ 8845 type: 'warn', 8846 message: 'The MPD manifest should contain no more than one ContentSteering tag' 8847 }); 8848 } // Return a null value if there are no ContentSteering tags 8849 8850 8851 if (!contentSteeringNodes.length) { 8852 return null; 8853 } 8854 8855 const infoFromContentSteeringTag = merge({ 8856 serverURL: getContent(contentSteeringNodes[0]) 8857 }, parseAttributes(contentSteeringNodes[0])); // Converts `queryBeforeStart` to a boolean, as well as setting the default value 8858 // to `false` if it doesn't exist 8859 8860 infoFromContentSteeringTag.queryBeforeStart = infoFromContentSteeringTag.queryBeforeStart === 'true'; 8861 return infoFromContentSteeringTag; 8862 }; 8863 /** 8864 * Gets Period@start property for a given period. 8865 * 8866 * @param {Object} options 8867 * Options object 8868 * @param {Object} options.attributes 8869 * Period attributes 8870 * @param {Object} [options.priorPeriodAttributes] 8871 * Prior period attributes (if prior period is available) 8872 * @param {string} options.mpdType 8873 * The MPD@type these periods came from 8874 * @return {number|null} 8875 * The period start, or null if it's an early available period or error 8876 */ 8877 8878 8879 const getPeriodStart = ({ 8880 attributes, 8881 priorPeriodAttributes, 8882 mpdType 8883 }) => { 8884 // Summary of period start time calculation from DASH spec section 5.3.2.1 8885 // 8886 // A period's start is the first period's start + time elapsed after playing all 8887 // prior periods to this one. Periods continue one after the other in time (without 8888 // gaps) until the end of the presentation. 8889 // 8890 // The value of Period@start should be: 8891 // 1. if Period@start is present: value of Period@start 8892 // 2. if previous period exists and it has @duration: previous Period@start + 8893 // previous Period@duration 8894 // 3. if this is first period and MPD@type is 'static': 0 8895 // 4. in all other cases, consider the period an "early available period" (note: not 8896 // currently supported) 8897 // (1) 8898 if (typeof attributes.start === 'number') { 8899 return attributes.start; 8900 } // (2) 8901 8902 8903 if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') { 8904 return priorPeriodAttributes.start + priorPeriodAttributes.duration; 8905 } // (3) 8906 8907 8908 if (!priorPeriodAttributes && mpdType === 'static') { 8909 return 0; 8910 } // (4) 8911 // There is currently no logic for calculating the Period@start value if there is 8912 // no Period@start or prior Period@start and Period@duration available. This is not made 8913 // explicit by the DASH interop guidelines or the DASH spec, however, since there's 8914 // nothing about any other resolution strategies, it's implied. Thus, this case should 8915 // be considered an early available period, or error, and null should suffice for both 8916 // of those cases. 8917 8918 8919 return null; 8920 }; 8921 /** 8922 * Traverses the mpd xml tree to generate a list of Representation information objects 8923 * that have inherited attributes from parent nodes 8924 * 8925 * @param {Node} mpd 8926 * The root node of the mpd 8927 * @param {Object} options 8928 * Available options for inheritAttributes 8929 * @param {string} options.manifestUri 8930 * The uri source of the mpd 8931 * @param {number} options.NOW 8932 * Current time per DASH IOP. Default is current time in ms since epoch 8933 * @param {number} options.clientOffset 8934 * Client time difference from NOW (in milliseconds) 8935 * @return {RepresentationInformation[]} 8936 * List of objects containing Representation information 8937 */ 8938 8939 8940 const inheritAttributes = (mpd, options = {}) => { 8941 const { 8942 manifestUri = '', 8943 NOW = Date.now(), 8944 clientOffset = 0, 8945 // TODO: For now, we are expecting an eventHandler callback function 8946 // to be passed into the mpd parser as an option. 8947 // In the future, we should enable stream parsing by using the Stream class from vhs-utils. 8948 // This will support new features including a standardized event handler. 8949 // See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing. 8950 // https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9 8951 eventHandler = function () {} 8952 } = options; 8953 const periodNodes = findChildren(mpd, 'Period'); 8954 8955 if (!periodNodes.length) { 8956 throw new Error(errors.INVALID_NUMBER_OF_PERIOD); 8957 } 8958 8959 const locations = findChildren(mpd, 'Location'); 8960 const mpdAttributes = parseAttributes(mpd); 8961 const mpdBaseUrls = buildBaseUrls([{ 8962 baseUrl: manifestUri 8963 }], findChildren(mpd, 'BaseURL')); 8964 const contentSteeringNodes = findChildren(mpd, 'ContentSteering'); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'. 8965 8966 mpdAttributes.type = mpdAttributes.type || 'static'; 8967 mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; 8968 mpdAttributes.NOW = NOW; 8969 mpdAttributes.clientOffset = clientOffset; 8970 8971 if (locations.length) { 8972 mpdAttributes.locations = locations.map(getContent); 8973 } 8974 8975 const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to 8976 // adding properties that require looking at prior periods is to parse attributes and add 8977 // missing ones before toAdaptationSets is called. If more such properties are added, it 8978 // may be better to refactor toAdaptationSets. 8979 8980 periodNodes.forEach((node, index) => { 8981 const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary 8982 // for this period. 8983 8984 const priorPeriod = periods[index - 1]; 8985 attributes.start = getPeriodStart({ 8986 attributes, 8987 priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null, 8988 mpdType: mpdAttributes.type 8989 }); 8990 periods.push({ 8991 node, 8992 attributes 8993 }); 8994 }); 8995 return { 8996 locations: mpdAttributes.locations, 8997 contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler), 8998 // TODO: There are occurences where this `representationInfo` array contains undesired 8999 // duplicates. This generally occurs when there are multiple BaseURL nodes that are 9000 // direct children of the MPD node. When we attempt to resolve URLs from a combination of the 9001 // parent BaseURL and a child BaseURL, and the value does not resolve, 9002 // we end up returning the child BaseURL multiple times. 9003 // We need to determine a way to remove these duplicates in a safe way. 9004 // See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527 9005 representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))), 9006 eventStream: flatten(periods.map(toEventStream)) 9007 }; 9008 }; 9009 9010 const stringToMpdXml = manifestString => { 9011 if (manifestString === '') { 9012 throw new Error(errors.DASH_EMPTY_MANIFEST); 9013 } 9014 9015 const parser = new xmldom.DOMParser(); 9016 let xml; 9017 let mpd; 9018 9019 try { 9020 xml = parser.parseFromString(manifestString, 'application/xml'); 9021 mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; 9022 } catch (e) {// ie 11 throws on invalid xml 9023 } 9024 9025 if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { 9026 throw new Error(errors.DASH_INVALID_XML); 9027 } 9028 9029 return mpd; 9030 }; 9031 /** 9032 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 9033 * 9034 * @param {string} mpd 9035 * XML string of the MPD manifest 9036 * @return {Object|null} 9037 * Attributes of UTCTiming node specified in the manifest. Null if none found 9038 */ 9039 9040 9041 const parseUTCTimingScheme = mpd => { 9042 const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; 9043 9044 if (!UTCTimingNode) { 9045 return null; 9046 } 9047 9048 const attributes = parseAttributes(UTCTimingNode); 9049 9050 switch (attributes.schemeIdUri) { 9051 case 'urn:mpeg:dash:utc:http-head:2014': 9052 case 'urn:mpeg:dash:utc:http-head:2012': 9053 attributes.method = 'HEAD'; 9054 break; 9055 9056 case 'urn:mpeg:dash:utc:http-xsdate:2014': 9057 case 'urn:mpeg:dash:utc:http-iso:2014': 9058 case 'urn:mpeg:dash:utc:http-xsdate:2012': 9059 case 'urn:mpeg:dash:utc:http-iso:2012': 9060 attributes.method = 'GET'; 9061 break; 9062 9063 case 'urn:mpeg:dash:utc:direct:2014': 9064 case 'urn:mpeg:dash:utc:direct:2012': 9065 attributes.method = 'DIRECT'; 9066 attributes.value = Date.parse(attributes.value); 9067 break; 9068 9069 case 'urn:mpeg:dash:utc:http-ntp:2014': 9070 case 'urn:mpeg:dash:utc:ntp:2014': 9071 case 'urn:mpeg:dash:utc:sntp:2014': 9072 default: 9073 throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); 9074 } 9075 9076 return attributes; 9077 }; 9078 /* 9079 * Given a DASH manifest string and options, parses the DASH manifest into an object in the 9080 * form outputed by m3u8-parser and accepted by videojs/http-streaming. 9081 * 9082 * For live DASH manifests, if `previousManifest` is provided in options, then the newly 9083 * parsed DASH manifest will have its media sequence and discontinuity sequence values 9084 * updated to reflect its position relative to the prior manifest. 9085 * 9086 * @param {string} manifestString - the DASH manifest as a string 9087 * @param {options} [options] - any options 9088 * 9089 * @return {Object} the manifest object 9090 */ 9091 9092 const parse = (manifestString, options = {}) => { 9093 const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options); 9094 const playlists = toPlaylists(parsedManifestInfo.representationInfo); 9095 return toM3u8({ 9096 dashPlaylists: playlists, 9097 locations: parsedManifestInfo.locations, 9098 contentSteering: parsedManifestInfo.contentSteeringInfo, 9099 sidxMapping: options.sidxMapping, 9100 previousManifest: options.previousManifest, 9101 eventStream: parsedManifestInfo.eventStream 9102 }); 9103 }; 9104 /** 9105 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 9106 * 9107 * @param {string} manifestString 9108 * XML string of the MPD manifest 9109 * @return {Object|null} 9110 * Attributes of UTCTiming node specified in the manifest. Null if none found 9111 */ 9112 9113 9114 const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString)); 9115 9116 var MAX_UINT32 = Math.pow(2, 32); 9117 9118 var getUint64$1 = function (uint8) { 9119 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 9120 var value; 9121 9122 if (dv.getBigUint64) { 9123 value = dv.getBigUint64(0); 9124 9125 if (value < Number.MAX_SAFE_INTEGER) { 9126 return Number(value); 9127 } 9128 9129 return value; 9130 } 9131 9132 return dv.getUint32(0) * MAX_UINT32 + dv.getUint32(4); 9133 }; 9134 9135 var numbers = { 9136 getUint64: getUint64$1, 9137 MAX_UINT32: MAX_UINT32 9138 }; 9139 9140 var getUint64 = numbers.getUint64; 9141 9142 var parseSidx = function (data) { 9143 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 9144 result = { 9145 version: data[0], 9146 flags: new Uint8Array(data.subarray(1, 4)), 9147 references: [], 9148 referenceId: view.getUint32(4), 9149 timescale: view.getUint32(8) 9150 }, 9151 i = 12; 9152 9153 if (result.version === 0) { 9154 result.earliestPresentationTime = view.getUint32(i); 9155 result.firstOffset = view.getUint32(i + 4); 9156 i += 8; 9157 } else { 9158 // read 64 bits 9159 result.earliestPresentationTime = getUint64(data.subarray(i)); 9160 result.firstOffset = getUint64(data.subarray(i + 8)); 9161 i += 16; 9162 } 9163 9164 i += 2; // reserved 9165 9166 var referenceCount = view.getUint16(i); 9167 i += 2; // start of references 9168 9169 for (; referenceCount > 0; i += 12, referenceCount--) { 9170 result.references.push({ 9171 referenceType: (data[i] & 0x80) >>> 7, 9172 referencedSize: view.getUint32(i) & 0x7FFFFFFF, 9173 subsegmentDuration: view.getUint32(i + 4), 9174 startsWithSap: !!(data[i + 8] & 0x80), 9175 sapType: (data[i + 8] & 0x70) >>> 4, 9176 sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF 9177 }); 9178 } 9179 9180 return result; 9181 }; 9182 9183 var parseSidx_1 = parseSidx; 9184 9185 var ID3 = toUint8([0x49, 0x44, 0x33]); 9186 var getId3Size = function getId3Size(bytes, offset) { 9187 if (offset === void 0) { 9188 offset = 0; 9189 } 9190 9191 bytes = toUint8(bytes); 9192 var flags = bytes[offset + 5]; 9193 var returnSize = bytes[offset + 6] << 21 | bytes[offset + 7] << 14 | bytes[offset + 8] << 7 | bytes[offset + 9]; 9194 var footerPresent = (flags & 16) >> 4; 9195 9196 if (footerPresent) { 9197 return returnSize + 20; 9198 } 9199 9200 return returnSize + 10; 9201 }; 9202 var getId3Offset = function getId3Offset(bytes, offset) { 9203 if (offset === void 0) { 9204 offset = 0; 9205 } 9206 9207 bytes = toUint8(bytes); 9208 9209 if (bytes.length - offset < 10 || !bytesMatch(bytes, ID3, { 9210 offset: offset 9211 })) { 9212 return offset; 9213 } 9214 9215 offset += getId3Size(bytes, offset); // recursive check for id3 tags as some files 9216 // have multiple ID3 tag sections even though 9217 // they should not. 9218 9219 return getId3Offset(bytes, offset); 9220 }; 9221 9222 var normalizePath$1 = function normalizePath(path) { 9223 if (typeof path === 'string') { 9224 return stringToBytes(path); 9225 } 9226 9227 if (typeof path === 'number') { 9228 return path; 9229 } 9230 9231 return path; 9232 }; 9233 9234 var normalizePaths$1 = function normalizePaths(paths) { 9235 if (!Array.isArray(paths)) { 9236 return [normalizePath$1(paths)]; 9237 } 9238 9239 return paths.map(function (p) { 9240 return normalizePath$1(p); 9241 }); 9242 }; 9243 /** 9244 * find any number of boxes by name given a path to it in an iso bmff 9245 * such as mp4. 9246 * 9247 * @param {TypedArray} bytes 9248 * bytes for the iso bmff to search for boxes in 9249 * 9250 * @param {Uint8Array[]|string[]|string|Uint8Array}
vendor: 9,470 bytes, lines 9250-9565
9250 name 9251 * An array of paths or a single path representing the name 9252 * of boxes to search through in bytes. Paths may be 9253 * uint8 (character codes) or strings. 9254 * 9255 * @param {boolean} [complete=false] 9256 * Should we search only for complete boxes on the final path. 9257 * This is very useful when you do not want to get back partial boxes 9258 * in the case of streaming files. 9259 * 9260 * @return {Uint8Array[]} 9261 * An array of the end paths that we found. 9262 */ 9263 9264 var findBox = function findBox(bytes, paths, complete) { 9265 if (complete === void 0) { 9266 complete = false; 9267 } 9268 9269 paths = normalizePaths$1(paths); 9270 bytes = toUint8(bytes); 9271 var results = []; 9272 9273 if (!paths.length) { 9274 // short-circuit the search for empty paths 9275 return results; 9276 } 9277 9278 var i = 0; 9279 9280 while (i < bytes.length) { 9281 var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0; 9282 var type = bytes.subarray(i + 4, i + 8); // invalid box format. 9283 9284 if (size === 0) { 9285 break; 9286 } 9287 9288 var end = i + size; 9289 9290 if (end > bytes.length) { 9291 // this box is bigger than the number of bytes we have 9292 // and complete is set, we cannot find any more boxes. 9293 if (complete) { 9294 break; 9295 } 9296 9297 end = bytes.length; 9298 } 9299 9300 var data = bytes.subarray(i + 8, end); 9301 9302 if (bytesMatch(type, paths[0])) { 9303 if (paths.length === 1) { 9304 // this is the end of the path and we've found the box we were 9305 // looking for 9306 results.push(data); 9307 } else { 9308 // recursively search for the next box along the path 9309 results.push.apply(results, findBox(data, paths.slice(1), complete)); 9310 } 9311 } 9312 9313 i = end; 9314 } // we've finished searching all of bytes 9315 9316 9317 return results; 9318 }; 9319 9320 // https://matroska-org.github.io/libebml/specs.html 9321 // https://www.matroska.org/technical/elements.html 9322 // https://www.webmproject.org/docs/container/ 9323 9324 var EBML_TAGS = { 9325 EBML: toUint8([0x1A, 0x45, 0xDF, 0xA3]), 9326 DocType: toUint8([0x42, 0x82]), 9327 Segment: toUint8([0x18, 0x53, 0x80, 0x67]), 9328 SegmentInfo: toUint8([0x15, 0x49, 0xA9, 0x66]), 9329 Tracks: toUint8([0x16, 0x54, 0xAE, 0x6B]), 9330 Track: toUint8([0xAE]), 9331 TrackNumber: toUint8([0xd7]), 9332 DefaultDuration: toUint8([0x23, 0xe3, 0x83]), 9333 TrackEntry: toUint8([0xAE]), 9334 TrackType: toUint8([0x83]), 9335 FlagDefault: toUint8([0x88]), 9336 CodecID: toUint8([0x86]), 9337 CodecPrivate: toUint8([0x63, 0xA2]), 9338 VideoTrack: toUint8([0xe0]), 9339 AudioTrack: toUint8([0xe1]), 9340 // Not used yet, but will be used for live webm/mkv 9341 // see https://www.matroska.org/technical/basics.html#block-structure 9342 // see https://www.matroska.org/technical/basics.html#simpleblock-structure 9343 Cluster: toUint8([0x1F, 0x43, 0xB6, 0x75]), 9344 Timestamp: toUint8([0xE7]), 9345 TimestampScale: toUint8([0x2A, 0xD7, 0xB1]), 9346 BlockGroup: toUint8([0xA0]), 9347 BlockDuration: toUint8([0x9B]), 9348 Block: toUint8([0xA1]), 9349 SimpleBlock: toUint8([0xA3]) 9350 }; 9351 /** 9352 * This is a simple table to determine the length 9353 * of things in ebml. The length is one based (starts at 1, 9354 * rather than zero) and for every zero bit before a one bit 9355 * we add one to length. We also need this table because in some 9356 * case we have to xor all the length bits from another value. 9357 */ 9358 9359 var LENGTH_TABLE = [128, 64, 32, 16, 8, 4, 2, 1]; 9360 9361 var getLength = function getLength(byte) { 9362 var len = 1; 9363 9364 for (var i = 0; i < LENGTH_TABLE.length; i++) { 9365 if (byte & LENGTH_TABLE[i]) { 9366 break; 9367 } 9368 9369 len++; 9370 } 9371 9372 return len; 9373 }; // length in ebml is stored in the first 4 to 8 bits 9374 // of the first byte. 4 for the id length and 8 for the 9375 // data size length. Length is measured by converting the number to binary 9376 // then 1 + the number of zeros before a 1 is encountered starting 9377 // from the left. 9378 9379 9380 var getvint = function getvint(bytes, offset, removeLength, signed) { 9381 if (removeLength === void 0) { 9382 removeLength = true; 9383 } 9384 9385 if (signed === void 0) { 9386 signed = false; 9387 } 9388 9389 var length = getLength(bytes[offset]); 9390 var valueBytes = bytes.subarray(offset, offset + length); // NOTE that we do **not** subarray here because we need to copy these bytes 9391 // as they will be modified below to remove the dataSizeLen bits and we do not 9392 // want to modify the original data. normally we could just call slice on 9393 // uint8array but ie 11 does not support that... 9394 9395 if (removeLength) { 9396 valueBytes = Array.prototype.slice.call(bytes, offset, offset + length); 9397 valueBytes[0] ^= LENGTH_TABLE[length - 1]; 9398 } 9399 9400 return { 9401 length: length, 9402 value: bytesToNumber(valueBytes, { 9403 signed: signed 9404 }), 9405 bytes: valueBytes 9406 }; 9407 }; 9408 9409 var normalizePath = function normalizePath(path) { 9410 if (typeof path === 'string') { 9411 return path.match(/.{1,2}/g).map(function (p) { 9412 return normalizePath(p); 9413 }); 9414 } 9415 9416 if (typeof path === 'number') { 9417 return numberToBytes(path); 9418 } 9419 9420 return path; 9421 }; 9422 9423 var normalizePaths = function normalizePaths(paths) { 9424 if (!Array.isArray(paths)) { 9425 return [normalizePath(paths)]; 9426 } 9427 9428 return paths.map(function (p) { 9429 return normalizePath(p); 9430 }); 9431 }; 9432 9433 var getInfinityDataSize = function getInfinityDataSize(id, bytes, offset) { 9434 if (offset >= bytes.length) { 9435 return bytes.length; 9436 } 9437 9438 var innerid = getvint(bytes, offset, false); 9439 9440 if (bytesMatch(id.bytes, innerid.bytes)) { 9441 return offset; 9442 } 9443 9444 var dataHeader = getvint(bytes, offset + innerid.length); 9445 return getInfinityDataSize(id, bytes, offset + dataHeader.length + dataHeader.value + innerid.length); 9446 }; 9447 /** 9448 * Notes on the EBLM format. 9449 * 9450 * EBLM uses "vints" tags. Every vint tag contains 9451 * two parts 9452 * 9453 * 1. The length from the first byte. You get this by 9454 * converting the byte to binary and counting the zeros 9455 * before a 1. Then you add 1 to that. Examples 9456 * 00011111 = length 4 because there are 3 zeros before a 1. 9457 * 00100000 = length 3 because there are 2 zeros before a 1. 9458 * 00000011 = length 7 because there are 6 zeros before a 1. 9459 * 9460 * 2. The bits used for length are removed from the first byte 9461 * Then all the bytes are merged into a value. NOTE: this 9462 * is not the case for id ebml tags as there id includes 9463 * length bits. 9464 * 9465 */ 9466 9467 9468 var findEbml = function findEbml(bytes, paths) { 9469 paths = normalizePaths(paths); 9470 bytes = toUint8(bytes); 9471 var results = []; 9472 9473 if (!paths.length) { 9474 return results; 9475 } 9476 9477 var i = 0; 9478 9479 while (i < bytes.length) { 9480 var id = getvint(bytes, i, false); 9481 var dataHeader = getvint(bytes, i + id.length); 9482 var dataStart = i + id.length + dataHeader.length; // dataSize is unknown or this is a live stream 9483 9484 if (dataHeader.value === 0x7f) { 9485 dataHeader.value = getInfinityDataSize(id, bytes, dataStart); 9486 9487 if (dataHeader.value !== bytes.length) { 9488 dataHeader.value -= dataStart; 9489 } 9490 } 9491 9492 var dataEnd = dataStart + dataHeader.value > bytes.length ? bytes.length : dataStart + dataHeader.value; 9493 var data = bytes.subarray(dataStart, dataEnd); 9494 9495 if (bytesMatch(paths[0], id.bytes)) { 9496 if (paths.length === 1) { 9497 // this is the end of the paths and we've found the tag we were 9498 // looking for 9499 results.push(data); 9500 } else { 9501 // recursively search for the next tag inside of the data 9502 // of this one 9503 results = results.concat(findEbml(data, paths.slice(1))); 9504 } 9505 } 9506 9507 var totalLength = id.length + dataHeader.length + data.length; // move past this tag entirely, we are not looking for it 9508 9509 i += totalLength; 9510 } 9511 9512 return results; 9513 }; // see https://www.matroska.org/technical/basics.html#block-structure 9514 9515 var NAL_TYPE_ONE = toUint8([0x00, 0x00, 0x00, 0x01]); 9516 var NAL_TYPE_TWO = toUint8([0x00, 0x00, 0x01]); 9517 var EMULATION_PREVENTION = toUint8([0x00, 0x00, 0x03]); 9518 /** 9519 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 9520 * Sequence Payload" 9521 * 9522 * @param data {Uint8Array} the bytes of a RBSP from a NAL 9523 * unit 9524 * @return {Uint8Array} the RBSP without any Emulation 9525 * Prevention Bytes 9526 */ 9527 9528 var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(bytes) { 9529 var positions = []; 9530 var i = 1; // Find all `Emulation Prevention Bytes` 9531 9532 while (i < bytes.length - 2) { 9533 if (bytesMatch(bytes.subarray(i, i + 3), EMULATION_PREVENTION)) { 9534 positions.push(i + 2); 9535 i++; 9536 } 9537 9538 i++; 9539 } // If no Emulation Prevention Bytes were found just return the original 9540 // array 9541 9542 9543 if (positions.length === 0) { 9544 return bytes; 9545 } // Create a new array to hold the NAL unit data 9546 9547 9548 var newLength = bytes.length - positions.length; 9549 var newData = new Uint8Array(newLength); 9550 var sourceIndex = 0; 9551 9552 for (i = 0; i < newLength; sourceIndex++, i++) { 9553 if (sourceIndex === positions[0]) { 9554 // Skip this byte 9555 sourceIndex++; // Remove this position index 9556 9557 positions.shift(); 9558 } 9559 9560 newData[i] = bytes[sourceIndex]; 9561 } 9562 9563 return newData; 9564 }; 9565 var findNal = function findNal(bytes, dataType, types, nalLimit) {
9566 if (nalLimit === void 0) { 9567 nalLimit = Infinity; 9568 }
vendor: 4,758 bytes, lines 9568-9723
9568 9569 9570 bytes = toUint8(bytes); 9571 types = [].concat(types); 9572 var i = 0; 9573 var nalStart; 9574 var nalsFound = 0; // keep searching until: 9575 // we reach the end of bytes 9576 // we reach the maximum number of nals they want to seach 9577 // NOTE: that we disregard nalLimit when we have found the start 9578 // of the nal we want so that we can find the end of the nal we want. 9579 9580 while (i < bytes.length && (nalsFound < nalLimit || nalStart)) { 9581 var nalOffset = void 0; 9582 9583 if (bytesMatch(bytes.subarray(i), NAL_TYPE_ONE)) { 9584 nalOffset = 4; 9585 } else if (bytesMatch(bytes.subarray(i), NAL_TYPE_TWO)) { 9586 nalOffset = 3; 9587 } // we are unsynced, 9588 // find the next nal unit 9589 9590 9591 if (!nalOffset) { 9592 i++; 9593 continue; 9594 } 9595 9596 nalsFound++; 9597 9598 if (nalStart) { 9599 return discardEmulationPreventionBytes(bytes.subarray(nalStart, i)); 9600 } 9601 9602 var nalType = void 0; 9603 9604 if (dataType === 'h264') { 9605 nalType = bytes[i + nalOffset] & 0x1f; 9606 } else if (dataType === 'h265') { 9607 nalType = bytes[i + nalOffset] >> 1 & 0x3f; 9608 } 9609 9610 if (types.indexOf(nalType) !== -1) { 9611 nalStart = i + nalOffset; 9612 } // nal header is 1 length for h264, and 2 for h265 9613 9614 9615 i += nalOffset + (dataType === 'h264' ? 1 : 2); 9616 } 9617 9618 return bytes.subarray(0, 0); 9619 }; 9620 var findH264Nal = function findH264Nal(bytes, type, nalLimit) { 9621 return findNal(bytes, 'h264', type, nalLimit); 9622 }; 9623 var findH265Nal = function findH265Nal(bytes, type, nalLimit) { 9624 return findNal(bytes, 'h265', type, nalLimit); 9625 }; 9626 9627 var CONSTANTS = { 9628 // "webm" string literal in hex 9629 'webm': toUint8([0x77, 0x65, 0x62, 0x6d]), 9630 // "matroska" string literal in hex 9631 'matroska': toUint8([0x6d, 0x61, 0x74, 0x72, 0x6f, 0x73, 0x6b, 0x61]), 9632 // "fLaC" string literal in hex 9633 'flac': toUint8([0x66, 0x4c, 0x61, 0x43]), 9634 // "OggS" string literal in hex 9635 'ogg': toUint8([0x4f, 0x67, 0x67, 0x53]), 9636 // ac-3 sync byte, also works for ec-3 as that is simply a codec 9637 // of ac-3 9638 'ac3': toUint8([0x0b, 0x77]), 9639 // "RIFF" string literal in hex used for wav and avi 9640 'riff': toUint8([0x52, 0x49, 0x46, 0x46]), 9641 // "AVI" string literal in hex 9642 'avi': toUint8([0x41, 0x56, 0x49]), 9643 // "WAVE" string literal in hex 9644 'wav': toUint8([0x57, 0x41, 0x56, 0x45]), 9645 // "ftyp3g" string literal in hex 9646 '3gp': toUint8([0x66, 0x74, 0x79, 0x70, 0x33, 0x67]), 9647 // "ftyp" string literal in hex 9648 'mp4': toUint8([0x66, 0x74, 0x79, 0x70]), 9649 // "styp" string literal in hex 9650 'fmp4': toUint8([0x73, 0x74, 0x79, 0x70]), 9651 // "ftypqt" string literal in hex 9652 'mov': toUint8([0x66, 0x74, 0x79, 0x70, 0x71, 0x74]), 9653 // moov string literal in hex 9654 'moov': toUint8([0x6D, 0x6F, 0x6F, 0x76]), 9655 // moof string literal in hex 9656 'moof': toUint8([0x6D, 0x6F, 0x6F, 0x66]) 9657 }; 9658 var _isLikely = { 9659 aac: function aac(bytes) { 9660 var offset = getId3Offset(bytes); 9661 return bytesMatch(bytes, [0xFF, 0x10], { 9662 offset: offset, 9663 mask: [0xFF, 0x16] 9664 }); 9665 }, 9666 mp3: function mp3(bytes) { 9667 var offset = getId3Offset(bytes); 9668 return bytesMatch(bytes, [0xFF, 0x02], { 9669 offset: offset, 9670 mask: [0xFF, 0x06] 9671 }); 9672 }, 9673 webm: function webm(bytes) { 9674 var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is webm 9675 9676 return bytesMatch(docType, CONSTANTS.webm); 9677 }, 9678 mkv: function mkv(bytes) { 9679 var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is matroska 9680 9681 return bytesMatch(docType, CONSTANTS.matroska); 9682 }, 9683 mp4: function mp4(bytes) { 9684 // if this file is another base media file format, it is not mp4 9685 if (_isLikely['3gp'](bytes) || _isLikely.mov(bytes)) { 9686 return false; 9687 } // if this file starts with a ftyp or styp box its mp4 9688 9689 9690 if (bytesMatch(bytes, CONSTANTS.mp4, { 9691 offset: 4 9692 }) || bytesMatch(bytes, CONSTANTS.fmp4, { 9693 offset: 4 9694 })) { 9695 return true; 9696 } // if this file starts with a moof/moov box its mp4 9697 9698 9699 if (bytesMatch(bytes, CONSTANTS.moof, { 9700 offset: 4 9701 }) || bytesMatch(bytes, CONSTANTS.moov, { 9702 offset: 4 9703 })) { 9704 return true; 9705 } 9706 }, 9707 mov: function mov(bytes) { 9708 return bytesMatch(bytes, CONSTANTS.mov, { 9709 offset: 4 9710 }); 9711 }, 9712 '3gp': function gp(bytes) { 9713 return bytesMatch(bytes, CONSTANTS['3gp'], { 9714 offset: 4 9715 }); 9716 }, 9717 ac3: function ac3(bytes) { 9718 var offset = getId3Offset(bytes); 9719 return bytesMatch(bytes, CONSTANTS.ac3, { 9720 offset: offset 9721 }); 9722 }, 9723 ts: function ts(bytes) {
vendor: 39,877 bytes, lines 9724-10981
9724 if (bytes.length < 189 && bytes.length >= 1) { 9725 return bytes[0] === 0x47; 9726 } 9727 9728 var i = 0; // check the first 376 bytes for two matching sync bytes 9729 9730 while (i + 188 < bytes.length && i < 188) { 9731 if (bytes[i] === 0x47 && bytes[i + 188] === 0x47) { 9732 return true; 9733 } 9734 9735 i += 1; 9736 } 9737 9738 return false; 9739 }, 9740 flac: function flac(bytes) { 9741 var offset = getId3Offset(bytes); 9742 return bytesMatch(bytes, CONSTANTS.flac, { 9743 offset: offset 9744 }); 9745 }, 9746 ogg: function ogg(bytes) { 9747 return bytesMatch(bytes, CONSTANTS.ogg); 9748 }, 9749 avi: function avi(bytes) { 9750 return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.avi, { 9751 offset: 8 9752 }); 9753 }, 9754 wav: function wav(bytes) { 9755 return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.wav, { 9756 offset: 8 9757 }); 9758 }, 9759 'h264': function h264(bytes) { 9760 // find seq_parameter_set_rbsp 9761 return findH264Nal(bytes, 7, 3).length; 9762 }, 9763 'h265': function h265(bytes) { 9764 // find video_parameter_set_rbsp or seq_parameter_set_rbsp 9765 return findH265Nal(bytes, [32, 33], 3).length; 9766 } 9767 }; // get all the isLikely functions 9768 // but make sure 'ts' is above h264 and h265 9769 // but below everything else as it is the least specific 9770 9771 var isLikelyTypes = Object.keys(_isLikely) // remove ts, h264, h265 9772 .filter(function (t) { 9773 return t !== 'ts' && t !== 'h264' && t !== 'h265'; 9774 }) // add it back to the bottom 9775 .concat(['ts', 'h264', 'h265']); // make sure we are dealing with uint8 data. 9776 9777 isLikelyTypes.forEach(function (type) { 9778 var isLikelyFn = _isLikely[type]; 9779 9780 _isLikely[type] = function (bytes) { 9781 return isLikelyFn(toUint8(bytes)); 9782 }; 9783 }); // export after wrapping 9784 9785 var isLikely = _isLikely; // A useful list of file signatures can be found here 9786 // https://en.wikipedia.org/wiki/List_of_file_signatures 9787 9788 var detectContainerForBytes = function detectContainerForBytes(bytes) { 9789 bytes = toUint8(bytes); 9790 9791 for (var i = 0; i < isLikelyTypes.length; i++) { 9792 var type = isLikelyTypes[i]; 9793 9794 if (isLikely[type](bytes)) { 9795 return type; 9796 } 9797 } 9798 9799 return ''; 9800 }; // fmp4 is not a container 9801 9802 var isLikelyFmp4MediaSegment = function isLikelyFmp4MediaSegment(bytes) { 9803 return findBox(bytes, ['moof']).length > 0; 9804 }; 9805 9806 // which will only happen if the request is complete. 9807 9808 const callbackOnCompleted = (request, cb) => { 9809 if (request.readyState === 4) { 9810 return cb(); 9811 } 9812 9813 return; 9814 }; 9815 9816 const containerRequest = (uri, xhr, cb, requestType) => { 9817 let bytes = []; 9818 let id3Offset; 9819 let finished = false; 9820 9821 const endRequestAndCallback = function (err, req, type, _bytes) { 9822 req.abort(); 9823 finished = true; 9824 return cb(err, req, type, _bytes); 9825 }; 9826 9827 const progressListener = function (error, request) { 9828 if (finished) { 9829 return; 9830 } 9831 9832 if (error) { 9833 error.metadata = getStreamingNetworkErrorMetadata({ 9834 requestType, 9835 request, 9836 error 9837 }); 9838 return endRequestAndCallback(error, request, '', bytes); 9839 } // grap the new part of content that was just downloaded 9840 9841 9842 const newPart = request.responseText.substring(bytes && bytes.byteLength || 0, request.responseText.length); // add that onto bytes 9843 9844 bytes = concatTypedArrays(bytes, stringToBytes(newPart, true)); 9845 id3Offset = id3Offset || getId3Offset(bytes); // we need at least 10 bytes to determine a type 9846 // or we need at least two bytes after an id3Offset 9847 9848 if (bytes.length < 10 || id3Offset && bytes.length < id3Offset + 2) { 9849 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 9850 } 9851 9852 const type = detectContainerForBytes(bytes); // if this looks like a ts segment but we don't have enough data 9853 // to see the second sync byte, wait until we have enough data 9854 // before declaring it ts 9855 9856 if (type === 'ts' && bytes.length < 188) { 9857 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 9858 } // this may be an unsynced ts segment 9859 // wait for 376 bytes before detecting no container 9860 9861 9862 if (!type && bytes.length < 376) { 9863 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 9864 } 9865 9866 return endRequestAndCallback(null, request, type, bytes); 9867 }; 9868 9869 const options = { 9870 uri, 9871 9872 beforeSend(request) { 9873 // this forces the browser to pass the bytes to us unprocessed 9874 request.overrideMimeType('text/plain; charset=x-user-defined'); 9875 request.addEventListener('progress', function ({ 9876 total, 9877 loaded 9878 }) { 9879 return callbackWrapper(request, null, { 9880 statusCode: request.status 9881 }, progressListener); 9882 }); 9883 } 9884 9885 }; 9886 const request = xhr(options, function (error, response) { 9887 return callbackWrapper(request, error, response, progressListener); 9888 }); 9889 return request; 9890 }; 9891 9892 const { 9893 EventTarget 9894 } = videojs__default["default"]; 9895 9896 const dashPlaylistUnchanged = function (a, b) { 9897 if (!isPlaylistUnchanged(a, b)) { 9898 return false; 9899 } // for dash the above check will often return true in scenarios where 9900 // the playlist actually has changed because mediaSequence isn't a 9901 // dash thing, and we often set it to 1. So if the playlists have the same amount 9902 // of segments we return true. 9903 // So for dash we need to make sure that the underlying segments are different. 9904 // if sidx changed then the playlists are different. 9905 9906 9907 if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) { 9908 return false; 9909 } else if (!a.sidx && b.sidx || a.sidx && !b.sidx) { 9910 return false; 9911 } // one or the other does not have segments 9912 // there was a change. 9913 9914 9915 if (a.segments && !b.segments || !a.segments && b.segments) { 9916 return false; 9917 } // neither has segments nothing changed 9918 9919 9920 if (!a.segments && !b.segments) { 9921 return true; 9922 } // check segments themselves 9923 9924 9925 for (let i = 0; i < a.segments.length; i++) { 9926 const aSegment = a.segments[i]; 9927 const bSegment = b.segments[i]; // if uris are different between segments there was a change 9928 9929 if (aSegment.uri !== bSegment.uri) { 9930 return false; 9931 } // neither segment has a byterange, there will be no byterange change. 9932 9933 9934 if (!aSegment.byterange && !bSegment.byterange) { 9935 continue; 9936 } 9937 9938 const aByterange = aSegment.byterange; 9939 const bByterange = bSegment.byterange; // if byterange only exists on one of the segments, there was a change. 9940 9941 if (aByterange && !bByterange || !aByterange && bByterange) { 9942 return false; 9943 } // if both segments have byterange with different offsets, there was a change. 9944 9945 9946 if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) { 9947 return false; 9948 } 9949 } // if everything was the same with segments, this is the same playlist. 9950 9951 9952 return true; 9953 }; 9954 /** 9955 * Use the representation IDs from the mpd object to create groupIDs, the NAME is set to mandatory representation 9956 * ID in the parser. This allows for continuous playout across periods with the same representation IDs 9957 * (continuous periods as defined in DASH-IF 3.2.12). This is assumed in the mpd-parser as well. If we want to support 9958 * periods without continuous playback this function may need modification as well as the parser. 9959 */ 9960 9961 9962 const dashGroupId = (type, group, label, playlist) => { 9963 // If the manifest somehow does not have an ID (non-dash compliant), use the label. 9964 const playlistId = playlist.attributes.NAME || label; 9965 return `placeholder-uri-${type}-${group}-${playlistId}`; 9966 }; 9967 /** 9968 * Parses the main XML string and updates playlist URI references. 9969 * 9970 * @param {Object} config 9971 * Object of arguments 9972 * @param {string} config.mainXml 9973 * The mpd XML 9974 * @param {string} config.srcUrl 9975 * The mpd URL 9976 * @param {Date} config.clientOffset 9977 * A time difference between server and client 9978 * @param {Object} config.sidxMapping 9979 * SIDX mappings for moof/mdat URIs and byte ranges 9980 * @return {Object} 9981 * The parsed mpd manifest object 9982 */ 9983 9984 9985 const parseMainXml = ({ 9986 mainXml, 9987 srcUrl, 9988 clientOffset, 9989 sidxMapping, 9990 previousManifest 9991 }) => { 9992 const manifest = parse(mainXml, { 9993 manifestUri: srcUrl, 9994 clientOffset, 9995 sidxMapping, 9996 previousManifest 9997 }); 9998 addPropertiesToMain(manifest, srcUrl, dashGroupId); 9999 return manifest; 10000 }; 10001 /** 10002 * Removes any mediaGroup labels that no longer exist in the newMain 10003 * 10004 * @param {Object} update 10005 * The previous mpd object being updated 10006 * @param {Object} newMain 10007 * The new mpd object 10008 */ 10009 10010 const removeOldMediaGroupLabels = (update, newMain) => { 10011 forEachMediaGroup$1(update, (properties, type, group, label) => { 10012 if (!newMain.mediaGroups[type][group] || !(label in newMain.mediaGroups[type][group])) { 10013 delete update.mediaGroups[type][group][label]; 10014 } 10015 }); 10016 }; 10017 /** 10018 * Returns a new main manifest that is the result of merging an updated main manifest 10019 * into the original version. 10020 * 10021 * @param {Object} oldMain 10022 * The old parsed mpd object 10023 * @param {Object} newMain 10024 * The updated parsed mpd object 10025 * @return {Object} 10026 * A new object representing the original main manifest with the updated media 10027 * playlists merged in 10028 */ 10029 10030 10031 const updateMain = (oldMain, newMain, sidxMapping) => { 10032 let noChanges = true; 10033 let update = merge$1(oldMain, { 10034 // These are top level properties that can be updated 10035 duration: newMain.duration, 10036 minimumUpdatePeriod: newMain.minimumUpdatePeriod, 10037 timelineStarts: newMain.timelineStarts 10038 }); // First update the playlists in playlist list 10039 10040 for (let i = 0; i < newMain.playlists.length; i++) { 10041 const playlist = newMain.playlists[i]; 10042 10043 if (playlist.sidx) { 10044 const sidxKey = generateSidxKey(playlist.sidx); // add sidx segments to the playlist if we have all the sidx info already 10045 10046 if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) { 10047 addSidxSegmentsToPlaylist$1(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri); 10048 } 10049 } 10050 10051 const playlistUpdate = updateMain$1(update, playlist, dashPlaylistUnchanged); 10052 10053 if (playlistUpdate) { 10054 update = playlistUpdate; 10055 noChanges = false; 10056 } 10057 } // Then update media group playlists 10058 10059 10060 forEachMediaGroup$1(newMain, (properties, type, group, label) => { 10061 if (properties.playlists && properties.playlists.length) { 10062 const id = properties.playlists[0].id; 10063 const playlistUpdate = updateMain$1(update, properties.playlists[0], dashPlaylistUnchanged); 10064 10065 if (playlistUpdate) { 10066 update = playlistUpdate; // add new mediaGroup label if it doesn't exist and assign the new mediaGroup. 10067 10068 if (!(label in update.mediaGroups[type][group])) { 10069 update.mediaGroups[type][group][label] = properties; 10070 } // update the playlist reference within media groups 10071 10072 10073 update.mediaGroups[type][group][label].playlists[0] = update.playlists[id]; 10074 noChanges = false; 10075 } 10076 } 10077 }); // remove mediaGroup labels and references that no longer exist in the newMain 10078 10079 removeOldMediaGroupLabels(update, newMain); 10080 10081 if (newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 10082 noChanges = false; 10083 } 10084 10085 if (noChanges) { 10086 return null; 10087 } 10088 10089 return update; 10090 }; // SIDX should be equivalent if the URI and byteranges of the SIDX match. 10091 // If the SIDXs have maps, the two maps should match, 10092 // both `a` and `b` missing SIDXs is considered matching. 10093 // If `a` or `b` but not both have a map, they aren't matching. 10094 10095 const equivalentSidx = (a, b) => { 10096 const neitherMap = Boolean(!a.map && !b.map); 10097 const equivalentMap = neitherMap || Boolean(a.map && b.map && a.map.byterange.offset === b.map.byterange.offset && a.map.byterange.length === b.map.byterange.length); 10098 return equivalentMap && a.uri === b.uri && a.byterange.offset === b.byterange.offset && a.byterange.length === b.byterange.length; 10099 }; // exported for testing 10100 10101 10102 const compareSidxEntry = (playlists, oldSidxMapping) => { 10103 const newSidxMapping = {}; 10104 10105 for (const id in playlists) { 10106 const playlist = playlists[id]; 10107 const currentSidxInfo = playlist.sidx; 10108 10109 if (currentSidxInfo) { 10110 const key = generateSidxKey(currentSidxInfo); 10111 10112 if (!oldSidxMapping[key]) { 10113 break; 10114 } 10115 10116 const savedSidxInfo = oldSidxMapping[key].sidxInfo; 10117 10118 if (equivalentSidx(savedSidxInfo, currentSidxInfo)) { 10119 newSidxMapping[key] = oldSidxMapping[key]; 10120 } 10121 } 10122 } 10123 10124 return newSidxMapping; 10125 }; 10126 /** 10127 * A function that filters out changed items as they need to be requested separately. 10128 * 10129 * The method is exported for testing 10130 * 10131 * @param {Object} main the parsed mpd XML returned via mpd-parser 10132 * @param {Object} oldSidxMapping the SIDX to compare against 10133 */ 10134 10135 const filterChangedSidxMappings = (main, oldSidxMapping) => { 10136 const videoSidx = compareSidxEntry(main.playlists, oldSidxMapping); 10137 let mediaGroupSidx = videoSidx; 10138 forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => { 10139 if (properties.playlists && properties.playlists.length) { 10140 const playlists = properties.playlists; 10141 mediaGroupSidx = merge$1(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping)); 10142 } 10143 }); 10144 return mediaGroupSidx; 10145 }; 10146 class DashPlaylistLoader extends EventTarget { 10147 // DashPlaylistLoader must accept either a src url or a playlist because subsequent 10148 // playlist loader setups from media groups will expect to be able to pass a playlist 10149 // (since there aren't external URLs to media playlists with DASH) 10150 constructor(srcUrlOrPlaylist, vhs, options = {}, mainPlaylistLoader) { 10151 super(); 10152 this.isPaused_ = true; 10153 this.mainPlaylistLoader_ = mainPlaylistLoader || this; 10154 10155 if (!mainPlaylistLoader) { 10156 this.isMain_ = true; 10157 } 10158 10159 const { 10160 withCredentials = false 10161 } = options; 10162 this.vhs_ = vhs; 10163 this.withCredentials = withCredentials; 10164 this.addMetadataToTextTrack = options.addMetadataToTextTrack; 10165 10166 if (!srcUrlOrPlaylist) { 10167 throw new Error('A non-empty playlist URL or object is required'); 10168 } // event naming? 10169 10170 10171 this.on('minimumUpdatePeriod', () => { 10172 this.refreshXml_(); 10173 }); // live playlist staleness timeout 10174 10175 this.on('mediaupdatetimeout', () => { 10176 this.refreshMedia_(this.media().id); 10177 }); 10178 this.state = 'HAVE_NOTHING'; 10179 this.loadedPlaylists_ = {}; 10180 this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state 10181 // The mainPlaylistLoader will be created with a string 10182 10183 if (this.isMain_) { 10184 this.mainPlaylistLoader_.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes 10185 // once multi-period is refactored 10186 10187 this.mainPlaylistLoader_.sidxMapping_ = {}; 10188 } else { 10189 this.childPlaylist_ = srcUrlOrPlaylist; 10190 } 10191 } 10192 10193 get isPaused() { 10194 return this.isPaused_; 10195 } 10196 10197 requestErrored_(err, request, startingState) { 10198 // disposed 10199 if (!this.request) { 10200 return true; 10201 } // pending request is cleared 10202 10203 10204 this.request = null; 10205 10206 if (err) { 10207 // use the provided error object or create one 10208 // based on the request/response 10209 this.error = typeof err === 'object' && !(err instanceof Error) ? err : { 10210 status: request.status, 10211 message: 'DASH request error at URL: ' + request.uri, 10212 response: request.response, 10213 // MEDIA_ERR_NETWORK 10214 code: 2, 10215 metadata: err.metadata 10216 }; 10217 10218 if (startingState) { 10219 this.state = startingState; 10220 } 10221 10222 this.trigger('error'); 10223 return true; 10224 } 10225 } 10226 /** 10227 * Verify that the container of the sidx segment can be parsed 10228 * and if it can, get and parse that segment. 10229 */ 10230 10231 10232 addSidxSegments_(playlist, startingState, cb) { 10233 const sidxKey = playlist.sidx && generateSidxKey(playlist.sidx); // playlist lacks sidx or sidx segments were added to this playlist already. 10234 10235 if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) { 10236 // keep this function async 10237 window.clearTimeout(this.mediaRequest_); 10238 this.mediaRequest_ = window.setTimeout(() => cb(false), 0); 10239 return; 10240 } // resolve the segment URL relative to the playlist 10241 10242 10243 const uri = resolveManifestRedirect(playlist.sidx.resolvedUri); 10244 10245 const fin = (err, request) => { 10246 if (this.requestErrored_(err, request, startingState)) { 10247 return; 10248 } 10249 10250 const sidxMapping = this.mainPlaylistLoader_.sidxMapping_; 10251 const { 10252 requestType 10253 } = request; 10254 let sidx; 10255 10256 try { 10257 sidx = parseSidx_1(toUint8(request.response).subarray(8)); 10258 } catch (e) { 10259 e.metadata = getStreamingNetworkErrorMetadata({ 10260 requestType, 10261 request, 10262 parseFailure: true 10263 }); // sidx parsing failed. 10264 10265 this.requestErrored_(e, request, startingState); 10266 return; 10267 } 10268 10269 sidxMapping[sidxKey] = { 10270 sidxInfo: playlist.sidx, 10271 sidx 10272 }; 10273 addSidxSegmentsToPlaylist$1(playlist, sidx, playlist.sidx.resolvedUri); 10274 return cb(true); 10275 }; 10276 10277 const REQUEST_TYPE = 'dash-sidx'; 10278 this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => { 10279 if (err) { 10280 return fin(err, request); 10281 } 10282 10283 if (!container || container !== 'mp4') { 10284 const sidxContainer = container || 'unknown'; 10285 return fin({ 10286 status: request.status, 10287 message: `Unsupported ${sidxContainer} container type for sidx segment at URL: ${uri}`, 10288 // response is just bytes in this case 10289 // but we really don't want to return that. 10290 response: '', 10291 playlist, 10292 internal: true, 10293 playlistExclusionDuration: Infinity, 10294 // MEDIA_ERR_NETWORK 10295 code: 2 10296 }, request); 10297 } // if we already downloaded the sidx bytes in the container request, use them 10298 10299 10300 const { 10301 offset, 10302 length 10303 } = playlist.sidx.byterange; 10304 10305 if (bytes.length >= length + offset) { 10306 return fin(err, { 10307 response: bytes.subarray(offset, offset + length), 10308 status: request.status, 10309 uri: request.uri 10310 }); 10311 } // otherwise request sidx bytes 10312 10313 10314 this.request = this.vhs_.xhr({ 10315 uri, 10316 responseType: 'arraybuffer', 10317 requestType: 'dash-sidx', 10318 headers: segmentXhrHeaders({ 10319 byterange: playlist.sidx.byterange 10320 }) 10321 }, fin); 10322 }, REQUEST_TYPE); 10323 } 10324 10325 dispose() { 10326 this.isPaused_ = true; 10327 this.trigger('dispose'); 10328 this.stopRequest(); 10329 this.loadedPlaylists_ = {}; 10330 window.clearTimeout(this.minimumUpdatePeriodTimeout_); 10331 window.clearTimeout(this.mediaRequest_); 10332 window.clearTimeout(this.mediaUpdateTimeout); 10333 this.mediaUpdateTimeout = null; 10334 this.mediaRequest_ = null; 10335 this.minimumUpdatePeriodTimeout_ = null; 10336 10337 if (this.mainPlaylistLoader_.createMupOnMedia_) { 10338 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 10339 this.mainPlaylistLoader_.createMupOnMedia_ = null; 10340 } 10341 10342 this.off(); 10343 } 10344 10345 hasPendingRequest() { 10346 return this.request || this.mediaRequest_; 10347 } 10348 10349 stopRequest() { 10350 if (this.request) { 10351 const oldRequest = this.request; 10352 this.request = null; 10353 oldRequest.onreadystatechange = null; 10354 oldRequest.abort(); 10355 } 10356 } 10357 10358 media(playlist) { 10359 // getter 10360 if (!playlist) { 10361 return this.media_; 10362 } // setter 10363 10364 10365 if (this.state === 'HAVE_NOTHING') { 10366 throw new Error('Cannot switch media playlist from ' + this.state); 10367 } 10368 10369 const startingState = this.state; // find the playlist object if the target playlist has been specified by URI 10370 10371 if (typeof playlist === 'string') { 10372 if (!this.mainPlaylistLoader_.main.playlists[playlist]) { 10373 throw new Error('Unknown playlist URI: ' + playlist); 10374 } 10375 10376 playlist = this.mainPlaylistLoader_.main.playlists[playlist]; 10377 } 10378 10379 const mediaChange = !this.media_ || playlist.id !== this.media_.id; // switch to previously loaded playlists immediately 10380 10381 if (mediaChange && this.loadedPlaylists_[playlist.id] && this.loadedPlaylists_[playlist.id].endList) { 10382 this.state = 'HAVE_METADATA'; 10383 this.media_ = playlist; // trigger media change if the active media has been updated 10384 10385 if (mediaChange) { 10386 this.trigger('mediachanging'); 10387 this.trigger('mediachange'); 10388 } 10389 10390 return; 10391 } // switching to the active playlist is a no-op 10392 10393 10394 if (!mediaChange) { 10395 return; 10396 } // switching from an already loaded playlist 10397 10398 10399 if (this.media_) { 10400 this.trigger('mediachanging'); 10401 } 10402 10403 this.addSidxSegments_(playlist, startingState, sidxChanged => { 10404 // everything is ready just continue to haveMetadata 10405 this.haveMetadata({ 10406 startingState, 10407 playlist 10408 }); 10409 }); 10410 } 10411 10412 haveMetadata({ 10413 startingState, 10414 playlist 10415 }) { 10416 this.state = 'HAVE_METADATA'; 10417 this.loadedPlaylists_[playlist.id] = playlist; 10418 window.clearTimeout(this.mediaRequest_); 10419 this.mediaRequest_ = null; // This will trigger loadedplaylist 10420 10421 this.refreshMedia_(playlist.id); // fire loadedmetadata the first time a media playlist is loaded 10422 // to resolve setup of media groups 10423 10424 if (startingState === 'HAVE_MAIN_MANIFEST') { 10425 this.trigger('loadedmetadata'); 10426 } else { 10427 // trigger media change if the active media has been updated 10428 this.trigger('mediachange'); 10429 } 10430 } 10431 10432 pause() { 10433 this.isPaused_ = true; 10434 10435 if (this.mainPlaylistLoader_.createMupOnMedia_) { 10436 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 10437 this.mainPlaylistLoader_.createMupOnMedia_ = null; 10438 } 10439 10440 this.stopRequest(); 10441 window.clearTimeout(this.mediaUpdateTimeout); 10442 this.mediaUpdateTimeout = null; 10443 10444 if (this.isMain_) { 10445 window.clearTimeout(this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_); 10446 this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_ = null; 10447 } 10448 10449 if (this.state === 'HAVE_NOTHING') { 10450 // If we pause the loader before any data has been retrieved, its as if we never 10451 // started, so reset to an unstarted state. 10452 this.started = false; 10453 } 10454 } 10455 10456 load(isFinalRendition) { 10457 this.isPaused_ = false; 10458 window.clearTimeout(this.mediaUpdateTimeout); 10459 this.mediaUpdateTimeout = null; 10460 const media = this.media(); 10461 10462 if (isFinalRendition) { 10463 const delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 10464 this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay); 10465 return; 10466 } // because the playlists are internal to the manifest, load should either load the 10467 // main manifest, or do nothing but trigger an event 10468 10469 10470 if (!this.started) { 10471 this.start(); 10472 return; 10473 } 10474 10475 if (media && !media.endList) { 10476 // Check to see if this is the main loader and the MUP was cleared (this happens 10477 // when the loader was paused). `media` should be set at this point since one is always 10478 // set during `start()`. 10479 if (this.isMain_ && !this.minimumUpdatePeriodTimeout_) { 10480 // Trigger minimumUpdatePeriod to refresh the main manifest 10481 this.trigger('minimumUpdatePeriod'); // Since there was no prior minimumUpdatePeriodTimeout it should be recreated 10482 10483 this.updateMinimumUpdatePeriodTimeout_(); 10484 } 10485 10486 this.trigger('mediaupdatetimeout'); 10487 } else { 10488 this.trigger('loadedplaylist'); 10489 } 10490 } 10491 10492 start() { 10493 this.started = true; // We don't need to request the main manifest again 10494 // Call this asynchronously to match the xhr request behavior below 10495 10496 if (!this.isMain_) { 10497 window.clearTimeout(this.mediaRequest_); 10498 this.mediaRequest_ = window.setTimeout(() => this.haveMain_(), 0); 10499 return; 10500 } 10501 10502 this.requestMain_((req, mainChanged) => { 10503 this.haveMain_(); 10504 10505 if (!this.hasPendingRequest() && !this.media_) { 10506 this.media(this.mainPlaylistLoader_.main.playlists[0]); 10507 } 10508 }); 10509 } 10510 10511 requestMain_(cb) { 10512 const metadata = { 10513 manifestInfo: { 10514 uri: this.mainPlaylistLoader_.srcUrl 10515 } 10516 }; 10517 this.trigger({ 10518 type: 'manifestrequeststart', 10519 metadata 10520 }); 10521 this.request = this.vhs_.xhr({ 10522 uri: this.mainPlaylistLoader_.srcUrl, 10523 withCredentials: this.withCredentials, 10524 requestType: 'dash-manifest' 10525 }, (error, req) => { 10526 if (error) { 10527 const { 10528 requestType 10529 } = req; 10530 error.metadata = getStreamingNetworkErrorMetadata({ 10531 requestType, 10532 request: req, 10533 error 10534 }); 10535 } 10536 10537 if (this.requestErrored_(error, req)) { 10538 if (this.state === 'HAVE_NOTHING') { 10539 this.started = false; 10540 } 10541 10542 return; 10543 } 10544 10545 this.trigger({ 10546 type: 'manifestrequestcomplete', 10547 metadata 10548 }); 10549 const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_; 10550 this.mainPlaylistLoader_.mainXml_ = req.responseText; 10551 10552 if (req.responseHeaders && req.responseHeaders.date) { 10553 this.mainLoaded_ = Date.parse(req.responseHeaders.date); 10554 } else { 10555 this.mainLoaded_ = Date.now(); 10556 } 10557 10558 this.mainPlaylistLoader_.srcUrl = resolveManifestRedirect(this.mainPlaylistLoader_.srcUrl, req); 10559 10560 if (mainChanged) { 10561 this.handleMain_(); 10562 this.syncClientServerClock_(() => { 10563 return cb(req, mainChanged); 10564 }); 10565 return; 10566 } 10567 10568 return cb(req, mainChanged); 10569 }); 10570 } 10571 /** 10572 * Parses the main xml for UTCTiming node to sync the client clock to the server 10573 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 10574 * 10575 * @param {Function} done 10576 * Function to call when clock sync has completed 10577 */ 10578 10579 10580 syncClientServerClock_(done) { 10581 const utcTiming = parseUTCTiming(this.mainPlaylistLoader_.mainXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the 10582 // server clock 10583 10584 if (utcTiming === null) { 10585 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 10586 return done(); 10587 } 10588 10589 if (utcTiming.method === 'DIRECT') { 10590 this.mainPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now(); 10591 return done(); 10592 } 10593 10594 this.request = this.vhs_.xhr({ 10595 uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value), 10596 method: utcTiming.method, 10597 withCredentials: this.withCredentials, 10598 requestType: 'dash-clock-sync' 10599 }, (error, req) => { 10600 // disposed 10601 if (!this.request) { 10602 return; 10603 } 10604 10605 if (error) { 10606 const { 10607 requestType 10608 } = req; 10609 this.error.metadata = getStreamingNetworkErrorMetadata({ 10610 requestType, 10611 request: req, 10612 error 10613 }); // sync request failed, fall back to using date header from mpd 10614 // TODO: log warning 10615 10616 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 10617 return done(); 10618 } 10619 10620 let serverTime; 10621 10622 if (utcTiming.method === 'HEAD') { 10623 if (!req.responseHeaders || !req.responseHeaders.date) { 10624 // expected date header not preset, fall back to using date header from mpd 10625 // TODO: log warning 10626 serverTime = this.mainLoaded_; 10627 } else { 10628 serverTime = Date.parse(req.responseHeaders.date); 10629 } 10630 } else { 10631 serverTime = Date.parse(req.responseText); 10632 } 10633 10634 this.mainPlaylistLoader_.clientOffset_ = serverTime - Date.now(); 10635 done(); 10636 }); 10637 } 10638 10639 haveMain_() { 10640 this.state = 'HAVE_MAIN_MANIFEST'; 10641 10642 if (this.isMain_) { 10643 // We have the main playlist at this point, so 10644 // trigger this to allow PlaylistController 10645 // to make an initial playlist selection 10646 this.trigger('loadedplaylist'); 10647 } else if (!this.media_) { 10648 // no media playlist was specifically selected so select 10649 // the one the child playlist loader was created with 10650 this.media(this.childPlaylist_); 10651 } 10652 } 10653 10654 handleMain_() { 10655 // clear media request 10656 window.clearTimeout(this.mediaRequest_); 10657 this.mediaRequest_ = null; 10658 const oldMain = this.mainPlaylistLoader_.main; 10659 const metadata = { 10660 manifestInfo: { 10661 uri: this.mainPlaylistLoader_.srcUrl 10662 } 10663 }; 10664 this.trigger({ 10665 type: 'manifestparsestart', 10666 metadata 10667 }); 10668 let newMain; 10669 10670 try { 10671 newMain = parseMainXml({ 10672 mainXml: this.mainPlaylistLoader_.mainXml_, 10673 srcUrl: this.mainPlaylistLoader_.srcUrl, 10674 clientOffset: this.mainPlaylistLoader_.clientOffset_, 10675 sidxMapping: this.mainPlaylistLoader_.sidxMapping_, 10676 previousManifest: oldMain 10677 }); 10678 } catch (error) { 10679 this.error = error; 10680 this.error.metadata = { 10681 errorType: videojs__default["default"].Error.StreamingDashManifestParserError, 10682 error 10683 }; 10684 this.trigger('error'); 10685 } // if we have an old main to compare the new main against 10686 10687 10688 if (oldMain) { 10689 newMain = updateMain(oldMain, newMain, this.mainPlaylistLoader_.sidxMapping_); 10690 } // only update main if we have a new main 10691 10692 10693 this.mainPlaylistLoader_.main = newMain ? newMain : oldMain; 10694 const location = this.mainPlaylistLoader_.main.locations && this.mainPlaylistLoader_.main.locations[0]; 10695 10696 if (location && location !== this.mainPlaylistLoader_.srcUrl) { 10697 this.mainPlaylistLoader_.srcUrl = location; 10698 } 10699 10700 if (!oldMain || newMain && newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 10701 this.updateMinimumUpdatePeriodTimeout_(); 10702 } 10703 10704 this.addEventStreamToMetadataTrack_(newMain); 10705 10706 if (newMain) { 10707 const { 10708 duration, 10709 endList 10710 } = newMain; 10711 const renditions = []; 10712 newMain.playlists.forEach(playlist => { 10713 renditions.push({ 10714 id: playlist.id, 10715 bandwidth: playlist.attributes.BANDWIDTH, 10716 resolution: playlist.attributes.RESOLUTION, 10717 codecs: playlist.attributes.CODECS 10718 }); 10719 }); 10720 const parsedManifest = { 10721 duration, 10722 isLive: !endList, 10723 renditions 10724 }; 10725 metadata.parsedManifest = parsedManifest; 10726 this.trigger({ 10727 type: 'manifestparsecomplete', 10728 metadata 10729 }); 10730 } 10731 10732 return Boolean(newMain); 10733 } 10734 10735 updateMinimumUpdatePeriodTimeout_() { 10736 const mpl = this.mainPlaylistLoader_; // cancel any pending creation of mup on media 10737 // a new one will be added if needed. 10738 10739 if (mpl.createMupOnMedia_) { 10740 mpl.off('loadedmetadata', mpl.createMupOnMedia_); 10741 mpl.createMupOnMedia_ = null; 10742 } // clear any pending timeouts 10743 10744 10745 if (mpl.minimumUpdatePeriodTimeout_) { 10746 window.clearTimeout(mpl.minimumUpdatePeriodTimeout_); 10747 mpl.minimumUpdatePeriodTimeout_ = null; 10748 } 10749 10750 let mup = mpl.main && mpl.main.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current 10751 // MPD has no future validity, so a new one will need to be acquired when new 10752 // media segments are to be made available. Thus, we use the target duration 10753 // in this case 10754 10755 if (mup === 0) { 10756 if (mpl.media()) { 10757 mup = mpl.media().targetDuration * 1000; 10758 } else { 10759 mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_; 10760 mpl.one('loadedmetadata', mpl.createMupOnMedia_); 10761 } 10762 } // if minimumUpdatePeriod is invalid or <= zero, which 10763 // can happen when a live video becomes VOD. skip timeout 10764 // creation. 10765 10766 10767 if (typeof mup !== 'number' || mup <= 0) { 10768 if (mup < 0) { 10769 this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`); 10770 } 10771 10772 return; 10773 } 10774 10775 this.createMUPTimeout_(mup); 10776 } 10777 10778 createMUPTimeout_(mup) { 10779 const mpl = this.mainPlaylistLoader_; 10780 mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { 10781 mpl.minimumUpdatePeriodTimeout_ = null; 10782 mpl.trigger('minimumUpdatePeriod'); 10783 mpl.createMUPTimeout_(mup); 10784 }, mup); 10785 } 10786 /** 10787 * Sends request to refresh the main xml and updates the parsed main manifest 10788 */ 10789 10790 10791 refreshXml_() { 10792 this.requestMain_((req, mainChanged) => { 10793 if (!mainChanged) { 10794 return; 10795 } 10796 10797 if (this.media_) { 10798 this.media_ = this.mainPlaylistLoader_.main.playlists[this.media_.id]; 10799 } // This will filter out updated sidx info from the mapping 10800 10801 10802 this.mainPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.sidxMapping_); 10803 this.addSidxSegments_(this.media(), this.state, sidxChanged => { 10804 // TODO: do we need to reload the current playlist? 10805 this.refreshMedia_(this.media().id); 10806 }); 10807 }); 10808 } 10809 /** 10810 * Refreshes the media playlist by re-parsing the main xml and updating playlist 10811 * references. If this is an alternate loader, the updated parsed manifest is retrieved 10812 * from the main loader. 10813 */ 10814 10815 10816 refreshMedia_(mediaID) { 10817 if (!mediaID) { 10818 throw new Error('refreshMedia_ must take a media id'); 10819 } // for main we have to reparse the main xml 10820 // to re-create segments based on current timing values 10821 // which may change media. We only skip updating the main manifest 10822 // if this is the first time this.media_ is being set. 10823 // as main was just parsed in that case. 10824 10825 10826 if (this.media_ && this.isMain_) { 10827 this.handleMain_(); 10828 } 10829 10830 const playlists = this.mainPlaylistLoader_.main.playlists; 10831 const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID]; 10832 10833 if (mediaChanged) { 10834 this.media_ = playlists[mediaID]; 10835 } else { 10836 this.trigger('playlistunchanged'); 10837 } 10838 10839 if (!this.mediaUpdateTimeout) { 10840 const createMediaUpdateTimeout = () => { 10841 if (this.media().endList) { 10842 return; 10843 } 10844 10845 this.mediaUpdateTimeout = window.setTimeout(() => { 10846 this.trigger('mediaupdatetimeout'); 10847 createMediaUpdateTimeout(); 10848 }, refreshDelay(this.media(), Boolean(mediaChanged))); 10849 }; 10850 10851 createMediaUpdateTimeout(); 10852 } 10853 10854 this.trigger('loadedplaylist'); 10855 } 10856 /** 10857 * Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track. 10858 * 10859 * @param {manifest} newMain the newly parsed manifest 10860 */ 10861 10862 10863 addEventStreamToMetadataTrack_(newMain) { 10864 // Only add new event stream metadata if we have a new manifest. 10865 if (newMain && this.mainPlaylistLoader_.main.eventStream) { 10866 // convert EventStream to ID3-like data. 10867 const metadataArray = this.mainPlaylistLoader_.main.eventStream.map(eventStreamNode => { 10868 return { 10869 cueTime: eventStreamNode.start, 10870 frames: [{ 10871 data: eventStreamNode.messageData 10872 }] 10873 }; 10874 }); 10875 this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration); 10876 } 10877 } 10878 /** 10879 * Returns the key ID set from a playlist 10880 * 10881 * @param {playlist} playlist to fetch the key ID set from. 10882 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 10883 */ 10884 10885 10886 getKeyIdSet(playlist) { 10887 if (playlist.contentProtection) { 10888 const keyIds = new Set(); 10889 10890 for (const keysystem in playlist.contentProtection) { 10891 const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID']; 10892 10893 if (defaultKID) { 10894 // DASH keyIds are separated by dashes. 10895 keyIds.add(defaultKID.replace(/-/g, '').toLowerCase()); 10896 } 10897 } 10898 10899 return keyIds; 10900 } 10901 } 10902 10903 } 10904 10905 var Config = { 10906 GOAL_BUFFER_LENGTH: 30, 10907 MAX_GOAL_BUFFER_LENGTH: 60, 10908 BACK_BUFFER_LENGTH: 30, 10909 GOAL_BUFFER_LENGTH_RATE: 1, 10910 // 0.5 MB/s 10911 INITIAL_BANDWIDTH: 4194304, 10912 // A fudge factor to apply to advertised playlist bitrates to account for 10913 // temporary flucations in client bandwidth 10914 BANDWIDTH_VARIANCE: 1.2, 10915 // How much of the buffer must be filled before we consider upswitching 10916 BUFFER_LOW_WATER_LINE: 0, 10917 MAX_BUFFER_LOW_WATER_LINE: 30, 10918 // TODO: Remove this when experimentalBufferBasedABR is removed 10919 EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16, 10920 BUFFER_LOW_WATER_LINE_RATE: 1, 10921 // If the buffer is greater than the high water line, we won't switch down 10922 BUFFER_HIGH_WATER_LINE: 30 10923 }; 10924 10925 const stringToArrayBuffer = string => { 10926 const view = new Uint8Array(new ArrayBuffer(string.length)); 10927 10928 for (let i = 0; i < string.length; i++) { 10929 view[i] = string.charCodeAt(i); 10930 } 10931 10932 return view.buffer; 10933 }; 10934 10935 /* global Blob, BlobBuilder, Worker */ 10936 // unify worker interface 10937 const browserWorkerPolyFill = function (workerObj) { 10938 // node only supports on/off 10939 workerObj.on = workerObj.addEventListener; 10940 workerObj.off = workerObj.removeEventListener; 10941 return workerObj; 10942 }; 10943 10944 const createObjectURL = function (str) { 10945 try { 10946 return URL.createObjectURL(new Blob([str], { 10947 type: 'application/javascript' 10948 })); 10949 } catch (e) { 10950 const blob = new BlobBuilder(); 10951 blob.append(str); 10952 return URL.createObjectURL(blob.getBlob()); 10953 } 10954 }; 10955 10956 const factory = function (code) { 10957 return function () { 10958 const objectUrl = createObjectURL(code); 10959 const worker = browserWorkerPolyFill(new Worker(objectUrl)); 10960 worker.objURL = objectUrl; 10961 const terminate = worker.terminate; 10962 worker.on = worker.addEventListener; 10963 worker.off = worker.removeEventListener; 10964 10965 worker.terminate = function () { 10966 URL.revokeObjectURL(objectUrl); 10967 return terminate.call(this); 10968 }; 10969 10970 return worker; 10971 }; 10972 }; 10973 const transform = function (code) { 10974 return `var browserWorkerPolyFill = ${browserWorkerPolyFill.toString()};\n` + 'browserWorkerPolyFill(self);\n' + code; 10975 }; 10976 10977 const getWorkerString = function (fn) { 10978 return fn.toString().replace(/^function.+?{/, '').slice(0, -1); 10979 }; 10980 10981 /* rollup-plugin-worker-factory start for worker!/
10981home/runner/work/http-streaming
vendor: 124,433 bytes, lines 10981-14729
10981/http-streaming/src/transmuxer-worker.js */ 10982 const workerCode$1 = transform(getWorkerString(function () { 10983 10984 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 10985 /** 10986 * mux.js 10987 * 10988 * Copyright (c) Brightcove 10989 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 10990 * 10991 * A lightweight readable stream implemention that handles event dispatching. 10992 * Objects that inherit from streams should call init in their constructors. 10993 */ 10994 10995 var Stream$8 = function () { 10996 this.init = function () { 10997 var listeners = {}; 10998 /** 10999 * Add a listener for a specified event type. 11000 * @param type {string} the event name 11001 * @param listener {function} the callback to be invoked when an event of 11002 * the specified type occurs 11003 */ 11004 11005 this.on = function (type, listener) { 11006 if (!listeners[type]) { 11007 listeners[type] = []; 11008 } 11009 11010 listeners[type] = listeners[type].concat(listener); 11011 }; 11012 /** 11013 * Remove a listener for a specified event type. 11014 * @param type {string} the event name 11015 * @param listener {function} a function previously registered for this 11016 * type of event through `on` 11017 */ 11018 11019 11020 this.off = function (type, listener) { 11021 var index; 11022 11023 if (!listeners[type]) { 11024 return false; 11025 } 11026 11027 index = listeners[type].indexOf(listener); 11028 listeners[type] = listeners[type].slice(); 11029 listeners[type].splice(index, 1); 11030 return index > -1; 11031 }; 11032 /** 11033 * Trigger an event of the specified type on this stream. Any additional 11034 * arguments to this function are passed as parameters to event listeners. 11035 * @param type {string} the event name 11036 */ 11037 11038 11039 this.trigger = function (type) { 11040 var callbacks, i, length, args; 11041 callbacks = listeners[type]; 11042 11043 if (!callbacks) { 11044 return; 11045 } // Slicing the arguments on every invocation of this method 11046 // can add a significant amount of overhead. Avoid the 11047 // intermediate object creation for the common case of a 11048 // single callback argument 11049 11050 11051 if (arguments.length === 2) { 11052 length = callbacks.length; 11053 11054 for (i = 0; i < length; ++i) { 11055 callbacks[i].call(this, arguments[1]); 11056 } 11057 } else { 11058 args = []; 11059 i = arguments.length; 11060 11061 for (i = 1; i < arguments.length; ++i) { 11062 args.push(arguments[i]); 11063 } 11064 11065 length = callbacks.length; 11066 11067 for (i = 0; i < length; ++i) { 11068 callbacks[i].apply(this, args); 11069 } 11070 } 11071 }; 11072 /** 11073 * Destroys the stream and cleans up. 11074 */ 11075 11076 11077 this.dispose = function () { 11078 listeners = {}; 11079 }; 11080 }; 11081 }; 11082 /** 11083 * Forwards all `data` events on this stream to the destination stream. The 11084 * destination stream should provide a method `push` to receive the data 11085 * events as they arrive. 11086 * @param destination {stream} the stream that will receive all `data` events 11087 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 11088 * when the current stream emits a 'done' event 11089 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 11090 */ 11091 11092 11093 Stream$8.prototype.pipe = function (destination) { 11094 this.on('data', function (data) { 11095 destination.push(data); 11096 }); 11097 this.on('done', function (flushSource) { 11098 destination.flush(flushSource); 11099 }); 11100 this.on('partialdone', function (flushSource) { 11101 destination.partialFlush(flushSource); 11102 }); 11103 this.on('endedtimeline', function (flushSource) { 11104 destination.endTimeline(flushSource); 11105 }); 11106 this.on('reset', function (flushSource) { 11107 destination.reset(flushSource); 11108 }); 11109 return destination; 11110 }; // Default stream functions that are expected to be overridden to perform 11111 // actual work. These are provided by the prototype as a sort of no-op 11112 // implementation so that we don't have to check for their existence in the 11113 // `pipe` function above. 11114 11115 11116 Stream$8.prototype.push = function (data) { 11117 this.trigger('data', data); 11118 }; 11119 11120 Stream$8.prototype.flush = function (flushSource) { 11121 this.trigger('done', flushSource); 11122 }; 11123 11124 Stream$8.prototype.partialFlush = function (flushSource) { 11125 this.trigger('partialdone', flushSource); 11126 }; 11127 11128 Stream$8.prototype.endTimeline = function (flushSource) { 11129 this.trigger('endedtimeline', flushSource); 11130 }; 11131 11132 Stream$8.prototype.reset = function (flushSource) { 11133 this.trigger('reset', flushSource); 11134 }; 11135 11136 var stream = Stream$8; 11137 var MAX_UINT32$1 = Math.pow(2, 32); 11138 11139 var getUint64$5 = function (uint8) { 11140 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 11141 var value; 11142 11143 if (dv.getBigUint64) { 11144 value = dv.getBigUint64(0); 11145 11146 if (value < Number.MAX_SAFE_INTEGER) { 11147 return Number(value); 11148 } 11149 11150 return value; 11151 } 11152 11153 return dv.getUint32(0) * MAX_UINT32$1 + dv.getUint32(4); 11154 }; 11155 11156 var numbers = { 11157 getUint64: getUint64$5, 11158 MAX_UINT32: MAX_UINT32$1 11159 }; 11160 /** 11161 * mux.js 11162 * 11163 * Copyright (c) Brightcove 11164 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11165 * 11166 * Functions that generate fragmented MP4s suitable for use with Media 11167 * Source Extensions. 11168 */ 11169 11170 var MAX_UINT32 = numbers.MAX_UINT32; 11171 var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun$1, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS; // pre-calculate constants 11172 11173 (function () { 11174 var i; 11175 types = { 11176 avc1: [], 11177 // codingname 11178 avcC: [], 11179 btrt: [], 11180 dinf: [], 11181 dref: [], 11182 esds: [], 11183 ftyp: [], 11184 hdlr: [], 11185 mdat: [], 11186 mdhd: [], 11187 mdia: [], 11188 mfhd: [], 11189 minf: [], 11190 moof: [], 11191 moov: [], 11192 mp4a: [], 11193 // codingname 11194 mvex: [], 11195 mvhd: [], 11196 pasp: [], 11197 sdtp: [], 11198 smhd: [], 11199 stbl: [], 11200 stco: [], 11201 stsc: [], 11202 stsd: [], 11203 stsz: [], 11204 stts: [], 11205 styp: [], 11206 tfdt: [], 11207 tfhd: [], 11208 traf: [], 11209 trak: [], 11210 trun: [], 11211 trex: [], 11212 tkhd: [], 11213 vmhd: [] 11214 }; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 11215 // don't throw an error 11216 11217 if (typeof Uint8Array === 'undefined') { 11218 return; 11219 } 11220 11221 for (i in types) { 11222 if (types.hasOwnProperty(i)) { 11223 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 11224 } 11225 } 11226 11227 MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]); 11228 AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]); 11229 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 11230 VIDEO_HDLR = new Uint8Array([0x00, // version 0 11231 0x00, 0x00, 0x00, // flags 11232 0x00, 0x00, 0x00, 0x00, // pre_defined 11233 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 11234 0x00, 0x00, 0x00, 0x00, // reserved 11235 0x00, 0x00, 0x00, 0x00, // reserved 11236 0x00, 0x00, 0x00, 0x00, // reserved 11237 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 11238 ]); 11239 AUDIO_HDLR = new Uint8Array([0x00, // version 0 11240 0x00, 0x00, 0x00, // flags 11241 0x00, 0x00, 0x00, 0x00, // pre_defined 11242 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 11243 0x00, 0x00, 0x00, 0x00, // reserved 11244 0x00, 0x00, 0x00, 0x00, // reserved 11245 0x00, 0x00, 0x00, 0x00, // reserved 11246 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 11247 ]); 11248 HDLR_TYPES = { 11249 video: VIDEO_HDLR, 11250 audio: AUDIO_HDLR 11251 }; 11252 DREF = new Uint8Array([0x00, // version 0 11253 0x00, 0x00, 0x00, // flags 11254 0x00, 0x00, 0x00, 0x01, // entry_count 11255 0x00, 0x00, 0x00, 0x0c, // entry_size 11256 0x75, 0x72, 0x6c, 0x20, // 'url' type 11257 0x00, // version 0 11258 0x00, 0x00, 0x01 // entry_flags 11259 ]); 11260 SMHD = new Uint8Array([0x00, // version 11261 0x00, 0x00, 0x00, // flags 11262 0x00, 0x00, // balance, 0 means centered 11263 0x00, 0x00 // reserved 11264 ]); 11265 STCO = new Uint8Array([0x00, // version 11266 0x00, 0x00, 0x00, // flags 11267 0x00, 0x00, 0x00, 0x00 // entry_count 11268 ]); 11269 STSC = STCO; 11270 STSZ = new Uint8Array([0x00, // version 11271 0x00, 0x00, 0x00, // flags 11272 0x00, 0x00, 0x00, 0x00, // sample_size 11273 0x00, 0x00, 0x00, 0x00 // sample_count 11274 ]); 11275 STTS = STCO; 11276 VMHD = new Uint8Array([0x00, // version 11277 0x00, 0x00, 0x01, // flags 11278 0x00, 0x00, // graphicsmode 11279 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 11280 ]); 11281 })(); 11282 11283 box = function (type) { 11284 var payload = [], 11285 size = 0, 11286 i, 11287 result, 11288 view; 11289 11290 for (i = 1; i < arguments.length; i++) { 11291 payload.push(arguments[i]); 11292 } 11293 11294 i = payload.length; // calculate the total size we need to allocate 11295 11296 while (i--) { 11297 size += payload[i].byteLength; 11298 } 11299 11300 result = new Uint8Array(size + 8); 11301 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 11302 view.setUint32(0, result.byteLength); 11303 result.set(type, 4); // copy the payload into the result 11304 11305 for (i = 0, size = 8; i < payload.length; i++) { 11306 result.set(payload[i], size); 11307 size += payload[i].byteLength; 11308 } 11309 11310 return result; 11311 }; 11312 11313 dinf = function () { 11314 return box(types.dinf, box(types.dref, DREF)); 11315 }; 11316 11317 esds = function (track) { 11318 return box(types.esds, new Uint8Array([0x00, // version 11319 0x00, 0x00, 0x00, // flags 11320 // ES_Descriptor 11321 0x03, // tag, ES_DescrTag 11322 0x19, // length 11323 0x00, 0x00, // ES_ID 11324 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority 11325 // DecoderConfigDescriptor 11326 0x04, // tag, DecoderConfigDescrTag 11327 0x11, // length 11328 0x40, // object type 11329 0x15, // streamType 11330 0x00, 0x06, 0x00, // bufferSizeDB 11331 0x00, 0x00, 0xda, 0xc0, // maxBitrate 11332 0x00, 0x00, 0xda, 0xc0, // avgBitrate 11333 // DecoderSpecificInfo 11334 0x05, // tag, DecoderSpecificInfoTag 11335 0x02, // length 11336 // ISO/IEC 14496-3, AudioSpecificConfig 11337 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 11338 track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig 11339 ])); 11340 }; 11341 11342 ftyp = function () { 11343 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 11344 }; 11345 11346 hdlr = function (type) { 11347 return box(types.hdlr, HDLR_TYPES[type]); 11348 }; 11349 11350 mdat = function (data) { 11351 return box(types.mdat, data); 11352 }; 11353 11354 mdhd = function (track) { 11355 var result = new Uint8Array([0x00, // version 0 11356 0x00, 0x00, 0x00, // flags 11357 0x00, 0x00, 0x00, 0x02, // creation_time 11358 0x00, 0x00, 0x00, 0x03, // modification_time 11359 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 11360 track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration 11361 0x55, 0xc4, // 'und' language (undetermined) 11362 0x00, 0x00]); // Use the sample rate from the track metadata, when it is 11363 // defined. The sample rate can be parsed out of an ADTS header, for 11364 // instance. 11365 11366 if (track.samplerate) { 11367 result[12] = track.samplerate >>> 24 & 0xFF; 11368 result[13] = track.samplerate >>> 16 & 0xFF; 11369 result[14] = track.samplerate >>> 8 & 0xFF; 11370 result[15] = track.samplerate & 0xFF; 11371 } 11372 11373 return box(types.mdhd, result); 11374 }; 11375 11376 mdia = function (track) { 11377 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 11378 }; 11379 11380 mfhd = function (sequenceNumber) { 11381 return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags 11382 (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number 11383 ])); 11384 }; 11385 11386 minf = function (track) { 11387 return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track)); 11388 }; 11389 11390 moof = function (sequenceNumber, tracks) { 11391 var trackFragments = [], 11392 i = tracks.length; // build traf boxes for each track fragment 11393 11394 while (i--) { 11395 trackFragments[i] = traf(tracks[i]); 11396 } 11397 11398 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 11399 }; 11400 /** 11401 * Returns a movie box. 11402 * @param tracks {array} the tracks associated with this movie 11403 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 11404 */ 11405 11406 11407 moov = function (tracks) { 11408 var i = tracks.length, 11409 boxes = []; 11410 11411 while (i--) { 11412 boxes[i] = trak(tracks[i]); 11413 } 11414 11415 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 11416 }; 11417 11418 mvex = function (tracks) { 11419 var i = tracks.length, 11420 boxes = []; 11421 11422 while (i--) { 11423 boxes[i] = trex(tracks[i]); 11424 } 11425 11426 return box.apply(null, [types.mvex].concat(boxes)); 11427 }; 11428 11429 mvhd = function (duration) { 11430 var bytes = new Uint8Array([0x00, // version 0 11431 0x00, 0x00, 0x00, // flags 11432 0x00, 0x00, 0x00, 0x01, // creation_time 11433 0x00, 0x00, 0x00, 0x02, // modification_time 11434 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 11435 (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration 11436 0x00, 0x01, 0x00, 0x00, // 1.0 rate 11437 0x01, 0x00, // 1.0 volume 11438 0x00, 0x00, // reserved 11439 0x00, 0x00, 0x00, 0x00, // reserved 11440 0x00, 0x00, 0x00, 0x00, // reserved 11441 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 11442 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 11443 0xff, 0xff, 0xff, 0xff // next_track_ID 11444 ]); 11445 return box(types.mvhd, bytes); 11446 }; 11447 11448 sdtp = function (track) { 11449 var samples = track.samples || [], 11450 bytes = new Uint8Array(4 + samples.length), 11451 flags, 11452 i; // leave the full box header (4 bytes) all zero 11453 // write the sample table 11454 11455 for (i = 0; i < samples.length; i++) { 11456 flags = samples[i].flags; 11457 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 11458 } 11459 11460 return box(types.sdtp, bytes); 11461 }; 11462 11463 stbl = function (track) { 11464 return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO)); 11465 }; 11466 11467 (function () { 11468 var videoSample, audioSample; 11469 11470 stsd = function (track) { 11471 return box(types.stsd, new Uint8Array([0x00, // version 0 11472 0x00, 0x00, 0x00, // flags 11473 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track)); 11474 }; 11475 11476 videoSample = function (track) { 11477 var sps = track.sps || [], 11478 pps = track.pps || [], 11479 sequenceParameterSets = [], 11480 pictureParameterSets = [], 11481 i, 11482 avc1Box; // assemble the SPSs 11483 11484 for (i = 0; i < sps.length; i++) { 11485 sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 11486 sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength 11487 11488 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 11489 } // assemble the PPSs 11490 11491 11492 for (i = 0; i < pps.length; i++) { 11493 pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 11494 pictureParameterSets.push(pps[i].byteLength & 0xFF); 11495 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 11496 } 11497 11498 avc1Box = [types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 11499 0x00, 0x01, // data_reference_index 11500 0x00, 0x00, // pre_defined 11501 0x00, 0x00, // reserved 11502 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined 11503 (track.width & 0xff00) >> 8, track.width & 0xff, // width 11504 (track.height & 0xff00) >> 8, track.height & 0xff, // height 11505 0x00, 0x48, 0x00, 0x00, // horizresolution 11506 0x00, 0x48, 0x00, 0x00, // vertresolution 11507 0x00, 0x00, 0x00, 0x00, // reserved 11508 0x00, 0x01, // frame_count 11509 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 11510 0x00, 0x18, // depth = 24 11511 0x11, 0x11 // pre_defined = -1 11512 ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion 11513 track.profileIdc, // AVCProfileIndication 11514 track.profileCompatibility, // profile_compatibility 11515 track.levelIdc, // AVCLevelIndication 11516 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 11517 ].concat([sps.length], // numOfSequenceParameterSets 11518 sequenceParameterSets, // "SPS" 11519 [pps.length], // numOfPictureParameterSets 11520 pictureParameterSets // "PPS" 11521 ))), box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 11522 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 11523 0x00, 0x2d, 0xc6, 0xc0 // avgBitrate 11524 ]))]; 11525 11526 if (track.sarRatio) { 11527 var hSpacing = track.sarRatio[0], 11528 vSpacing = track.sarRatio[1]; 11529 avc1Box.push(box(types.pasp, new Uint8Array([(hSpacing & 0xFF000000) >> 24, (hSpacing & 0xFF0000) >> 16, (hSpacing & 0xFF00) >> 8, hSpacing & 0xFF, (vSpacing & 0xFF000000) >> 24, (vSpacing & 0xFF0000) >> 16, (vSpacing & 0xFF00) >> 8, vSpacing & 0xFF]))); 11530 } 11531 11532 return box.apply(null, avc1Box); 11533 }; 11534 11535 audioSample = function (track) { 11536 return box(types.mp4a, new Uint8Array([// SampleEntry, ISO/IEC 14496-12 11537 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 11538 0x00, 0x01, // data_reference_index 11539 // AudioSampleEntry, ISO/IEC 14496-12 11540 0x00, 0x00, 0x00, 0x00, // reserved 11541 0x00, 0x00, 0x00, 0x00, // reserved 11542 (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount 11543 (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize 11544 0x00, 0x00, // pre_defined 11545 0x00, 0x00, // reserved 11546 (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16 11547 // MP4AudioSampleEntry, ISO/IEC 14496-14 11548 ]), esds(track)); 11549 }; 11550 })(); 11551 11552 tkhd = function (track) { 11553 var result = new Uint8Array([0x00, // version 0 11554 0x00, 0x00, 0x07, // flags 11555 0x00, 0x00, 0x00, 0x00, // creation_time 11556 0x00, 0x00, 0x00, 0x00, // modification_time 11557 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 11558 0x00, 0x00, 0x00, 0x00, // reserved 11559 (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration 11560 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 11561 0x00, 0x00, // layer 11562 0x00, 0x00, // alternate_group 11563 0x01, 0x00, // non-audio track volume 11564 0x00, 0x00, // reserved 11565 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 11566 (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width 11567 (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height 11568 ]); 11569 return box(types.tkhd, result); 11570 }; 11571 /** 11572 * Generate a track fragment (traf) box. A traf box collects metadata 11573 * about tracks in a movie fragment (moof) box. 11574 */ 11575 11576 11577 traf = function (track) { 11578 var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime; 11579 trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0 11580 0x00, 0x00, 0x3a, // flags 11581 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 11582 0x00, 0x00, 0x00, 0x01, // sample_description_index 11583 0x00, 0x00, 0x00, 0x00, // default_sample_duration 11584 0x00, 0x00, 0x00, 0x00, // default_sample_size 11585 0x00, 0x00, 0x00, 0x00 // default_sample_flags 11586 ])); 11587 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / MAX_UINT32); 11588 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % MAX_UINT32); 11589 trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1 11590 0x00, 0x00, 0x00, // flags 11591 // baseMediaDecodeTime 11592 upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF])); // the data offset specifies the number of bytes from the start of 11593 // the containing moof to the first payload byte of the associated 11594 // mdat 11595 11596 dataOffset = 32 + // tfhd 11597 20 + // tfdt 11598 8 + // traf header 11599 16 + // mfhd 11600 8 + // moof header 11601 8; // mdat header 11602 // audio tracks require less metadata 11603 11604 if (track.type === 'audio') { 11605 trackFragmentRun = trun$1(track, dataOffset); 11606 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 11607 } // video tracks should contain an independent and disposable samples 11608 // box (sdtp) 11609 // generate one and adjust offsets to match 11610 11611 11612 sampleDependencyTable = sdtp(track); 11613 trackFragmentRun = trun$1(track, sampleDependencyTable.length + dataOffset); 11614 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable); 11615 }; 11616 /** 11617 * Generate a track box. 11618 * @param track {object} a track definition 11619 * @return {Uint8Array} the track box 11620 */ 11621 11622 11623 trak = function (track) { 11624 track.duration = track.duration || 0xffffffff; 11625 return box(types.trak, tkhd(track), mdia(track)); 11626 }; 11627 11628 trex = function (track) { 11629 var result = new Uint8Array([0x00, // version 0 11630 0x00, 0x00, 0x00, // flags 11631 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 11632 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 11633 0x00, 0x00, 0x00, 0x00, // default_sample_duration 11634 0x00, 0x00, 0x00, 0x00, // default_sample_size 11635 0x00, 0x01, 0x00, 0x01 // default_sample_flags 11636 ]); // the last two bytes of default_sample_flags is the sample 11637 // degradation priority, a hint about the importance of this sample 11638 // relative to others. Lower the degradation priority for all sample 11639 // types other than video. 11640 11641 if (track.type !== 'video') { 11642 result[result.length - 1] = 0x00; 11643 } 11644 11645 return box(types.trex, result); 11646 }; 11647 11648 (function () { 11649 var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a 11650 // duration is present for the first sample, it will be present for 11651 // all subsequent samples. 11652 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 11653 11654 trunHeader = function (samples, offset) { 11655 var durationPresent = 0, 11656 sizePresent = 0, 11657 flagsPresent = 0, 11658 compositionTimeOffset = 0; // trun flag constants 11659 11660 if (samples.length) { 11661 if (samples[0].duration !== undefined) { 11662 durationPresent = 0x1; 11663 } 11664 11665 if (samples[0].size !== undefined) { 11666 sizePresent = 0x2; 11667 } 11668 11669 if (samples[0].flags !== undefined) { 11670 flagsPresent = 0x4; 11671 } 11672 11673 if (samples[0].compositionTimeOffset !== undefined) { 11674 compositionTimeOffset = 0x8; 11675 } 11676 } 11677 11678 return [0x00, // version 0 11679 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags 11680 (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count 11681 (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset 11682 ]; 11683 }; 11684 11685 videoTrun = function (track, offset) { 11686 var bytesOffest, bytes, header, samples, sample, i; 11687 samples = track.samples || []; 11688 offset += 8 + 12 + 16 * samples.length; 11689 header = trunHeader(samples, offset); 11690 bytes = new Uint8Array(header.length + samples.length * 16); 11691 bytes.set(header); 11692 bytesOffest = header.length; 11693 11694 for (i = 0; i < samples.length; i++) { 11695 sample = samples[i]; 11696 bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24; 11697 bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16; 11698 bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8; 11699 bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration 11700 11701 bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24; 11702 bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16; 11703 bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8; 11704 bytes[bytesOffest++] = sample.size & 0xFF; // sample_size 11705 11706 bytes[bytesOffest++] = sample.flags.isLeading << 2 | sample.flags.dependsOn; 11707 bytes[bytesOffest++] = sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample; 11708 bytes[bytesOffest++] = sample.flags.degradationPriority & 0xF0 << 8; 11709 bytes[bytesOffest++] = sample.flags.degradationPriority & 0x0F; // sample_flags 11710 11711 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF000000) >>> 24; 11712 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF0000) >>> 16; 11713 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF00) >>> 8; 11714 bytes[bytesOffest++] = sample.compositionTimeOffset & 0xFF; // sample_composition_time_offset 11715 } 11716 11717 return box(types.trun, bytes); 11718 }; 11719 11720 audioTrun = function (track, offset) { 11721 var bytes, bytesOffest, header, samples, sample, i; 11722 samples = track.samples || []; 11723 offset += 8 + 12 + 8 * samples.length; 11724 header = trunHeader(samples, offset); 11725 bytes = new Uint8Array(header.length + samples.length * 8); 11726 bytes.set(header); 11727 bytesOffest = header.length; 11728 11729 for (i = 0; i < samples.length; i++) { 11730 sample = samples[i]; 11731 bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24; 11732 bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16; 11733 bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8; 11734 bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration 11735 11736 bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24; 11737 bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16; 11738 bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8; 11739 bytes[bytesOffest++] = sample.size & 0xFF; // sample_size 11740 } 11741 11742 return box(types.trun, bytes); 11743 }; 11744 11745 trun$1 = function (track, offset) { 11746 if (track.type === 'audio') { 11747 return audioTrun(track, offset); 11748 } 11749 11750 return videoTrun(track, offset); 11751 }; 11752 })(); 11753 11754 var mp4Generator = { 11755 ftyp: ftyp, 11756 mdat: mdat, 11757 moof: moof, 11758 moov: moov, 11759 initSegment: function (tracks) { 11760 var fileType = ftyp(), 11761 movie = moov(tracks), 11762 result; 11763 result = new Uint8Array(fileType.byteLength + movie.byteLength); 11764 result.set(fileType); 11765 result.set(movie, fileType.byteLength); 11766 return result; 11767 } 11768 }; 11769 /** 11770 * mux.js 11771 * 11772 * Copyright (c) Brightcove 11773 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11774 */ 11775 // composed of the nal units that make up that frame 11776 // Also keep track of cummulative data about the frame from the nal units such 11777 // as the frame duration, starting pts, etc. 11778 11779 var groupNalsIntoFrames = function (nalUnits) { 11780 var i, 11781 currentNal, 11782 currentFrame = [], 11783 frames = []; // TODO added for LHLS, make sure this is OK 11784 11785 frames.byteLength = 0; 11786 frames.nalCount = 0; 11787 frames.duration = 0; 11788 currentFrame.byteLength = 0; 11789 11790 for (i = 0; i < nalUnits.length; i++) { 11791 currentNal = nalUnits[i]; // Split on 'aud'-type nal units 11792 11793 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 11794 // Since the very first nal unit is expected to be an AUD 11795 // only push to the frames array when currentFrame is not empty 11796 if (currentFrame.length) { 11797 currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK 11798 11799 frames.byteLength += currentFrame.byteLength; 11800 frames.nalCount += currentFrame.length; 11801 frames.duration += currentFrame.duration; 11802 frames.push(currentFrame); 11803 } 11804 11805 currentFrame = [currentNal]; 11806 currentFrame.byteLength = currentNal.data.byteLength; 11807 currentFrame.pts = currentNal.pts; 11808 currentFrame.dts = currentNal.dts; 11809 } else { 11810 // Specifically flag key frames for ease of use later 11811 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 11812 currentFrame.keyFrame = true; 11813 } 11814 11815 currentFrame.duration = currentNal.dts - currentFrame.dts; 11816 currentFrame.byteLength += currentNal.data.byteLength; 11817 currentFrame.push(currentNal); 11818 } 11819 } // For the last frame, use the duration of the previous frame if we 11820 // have nothing better to go on 11821 11822 11823 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 11824 currentFrame.duration = frames[frames.length - 1].duration; 11825 } // Push the final frame 11826 // TODO added for LHLS, make sure this is OK 11827 11828 11829 frames.byteLength += currentFrame.byteLength; 11830 frames.nalCount += currentFrame.length; 11831 frames.duration += currentFrame.duration; 11832 frames.push(currentFrame); 11833 return frames; 11834 }; // Convert an array of frames into an array of Gop with each Gop being composed 11835 // of the frames that make up that Gop 11836 // Also keep track of cummulative data about the Gop from the frames such as the 11837 // Gop duration, starting pts, etc. 11838 11839 11840 var groupFramesIntoGops = function (frames) { 11841 var i, 11842 currentFrame, 11843 currentGop = [], 11844 gops = []; // We must pre-set some of the values on the Gop since we 11845 // keep running totals of these values 11846 11847 currentGop.byteLength = 0; 11848 currentGop.nalCount = 0; 11849 currentGop.duration = 0; 11850 currentGop.pts = frames[0].pts; 11851 currentGop.dts = frames[0].dts; // store some metadata about all the Gops 11852 11853 gops.byteLength = 0; 11854 gops.nalCount = 0; 11855 gops.duration = 0; 11856 gops.pts = frames[0].pts; 11857 gops.dts = frames[0].dts; 11858 11859 for (i = 0; i < frames.length; i++) { 11860 currentFrame = frames[i]; 11861 11862 if (currentFrame.keyFrame) { 11863 // Since the very first frame is expected to be an keyframe 11864 // only push to the gops array when currentGop is not empty 11865 if (currentGop.length) { 11866 gops.push(currentGop); 11867 gops.byteLength += currentGop.byteLength; 11868 gops.nalCount += currentGop.nalCount; 11869 gops.duration += currentGop.duration; 11870 } 11871 11872 currentGop = [currentFrame]; 11873 currentGop.nalCount = currentFrame.length; 11874 currentGop.byteLength = currentFrame.byteLength; 11875 currentGop.pts = currentFrame.pts; 11876 currentGop.dts = currentFrame.dts; 11877 currentGop.duration = currentFrame.duration; 11878 } else { 11879 currentGop.duration += currentFrame.duration; 11880 currentGop.nalCount += currentFrame.length; 11881 currentGop.byteLength += currentFrame.byteLength; 11882 currentGop.push(currentFrame); 11883 } 11884 } 11885 11886 if (gops.length && currentGop.duration <= 0) { 11887 currentGop.duration = gops[gops.length - 1].duration; 11888 } 11889 11890 gops.byteLength += currentGop.byteLength; 11891 gops.nalCount += currentGop.nalCount; 11892 gops.duration += currentGop.duration; // push the final Gop 11893 11894 gops.push(currentGop); 11895 return gops; 11896 }; 11897 /* 11898 * Search for the first keyframe in the GOPs and throw away all frames 11899 * until that keyframe. Then extend the duration of the pulled keyframe 11900 * and pull the PTS and DTS of the keyframe so that it covers the time 11901 * range of the frames that were disposed. 11902 * 11903 * @param {Array} gops video GOPs 11904 * @returns {Array} modified video GOPs 11905 */ 11906 11907 11908 var extendFirstKeyFrame = function (gops) { 11909 var currentGop; 11910 11911 if (!gops[0][0].keyFrame && gops.length > 1) { 11912 // Remove the first GOP 11913 currentGop = gops.shift(); 11914 gops.byteLength -= currentGop.byteLength; 11915 gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the 11916 // first gop to cover the time period of the 11917 // frames we just removed 11918 11919 gops[0][0].dts = currentGop.dts; 11920 gops[0][0].pts = currentGop.pts; 11921 gops[0][0].duration += currentGop.duration; 11922 } 11923 11924 return gops; 11925 }; 11926 /** 11927 * Default sample object 11928 * see ISO/IEC 14496-12:2012, section 8.6.4.3 11929 */ 11930 11931 11932 var createDefaultSample = function () { 11933 return { 11934 size: 0, 11935 flags: { 11936 isLeading: 0, 11937 dependsOn: 1, 11938 isDependedOn: 0, 11939 hasRedundancy: 0, 11940 degradationPriority: 0, 11941 isNonSyncSample: 1 11942 } 11943 }; 11944 }; 11945 /* 11946 * Collates information from a video frame into an object for eventual 11947 * entry into an MP4 sample table. 11948 * 11949 * @param {Object} frame the video frame 11950 * @param {Number} dataOffset the byte offset to position the sample 11951 * @return {Object} object containing sample table info for a frame 11952 */ 11953 11954 11955 var sampleForFrame = function (frame, dataOffset) { 11956 var sample = createDefaultSample(); 11957 sample.dataOffset = dataOffset; 11958 sample.compositionTimeOffset = frame.pts - frame.dts; 11959 sample.duration = frame.duration; 11960 sample.size = 4 * frame.length; // Space for nal unit size 11961 11962 sample.size += frame.byteLength; 11963 11964 if (frame.keyFrame) { 11965 sample.flags.dependsOn = 2; 11966 sample.flags.isNonSyncSample = 0; 11967 } 11968 11969 return sample; 11970 }; // generate the track's sample table from an array of gops 11971 11972 11973 var generateSampleTable$1 = function (gops, baseDataOffset) { 11974 var h, 11975 i, 11976 sample, 11977 currentGop, 11978 currentFrame, 11979 dataOffset = baseDataOffset || 0, 11980 samples = []; 11981 11982 for (h = 0; h < gops.length; h++) { 11983 currentGop = gops[h]; 11984 11985 for (i = 0; i < currentGop.length; i++) { 11986 currentFrame = currentGop[i]; 11987 sample = sampleForFrame(currentFrame, dataOffset); 11988 dataOffset += sample.size; 11989 samples.push(sample); 11990 } 11991 } 11992 11993 return samples; 11994 }; // generate the track's raw mdat data from an array of gops 11995 11996 11997 var concatenateNalData = function (gops) { 11998 var h, 11999 i, 12000 j, 12001 currentGop, 12002 currentFrame, 12003 currentNal, 12004 dataOffset = 0, 12005 nalsByteLength = gops.byteLength, 12006 numberOfNals = gops.nalCount, 12007 totalByteLength = nalsByteLength + 4 * numberOfNals, 12008 data = new Uint8Array(totalByteLength), 12009 view = new DataView(data.buffer); // For each Gop.. 12010 12011 for (h = 0; h < gops.length; h++) { 12012 currentGop = gops[h]; // For each Frame.. 12013 12014 for (i = 0; i < currentGop.length; i++) { 12015 currentFrame = currentGop[i]; // For each NAL.. 12016 12017 for (j = 0; j < currentFrame.length; j++) { 12018 currentNal = currentFrame[j]; 12019 view.setUint32(dataOffset, currentNal.data.byteLength); 12020 dataOffset += 4; 12021 data.set(currentNal.data, dataOffset); 12022 dataOffset += currentNal.data.byteLength; 12023 } 12024 } 12025 } 12026 12027 return data; 12028 }; // generate the track's sample table from a frame 12029 12030 12031 var generateSampleTableForFrame = function (frame, baseDataOffset) { 12032 var sample, 12033 dataOffset = baseDataOffset || 0, 12034 samples = []; 12035 sample = sampleForFrame(frame, dataOffset); 12036 samples.push(sample); 12037 return samples; 12038 }; // generate the track's raw mdat data from a frame 12039 12040 12041 var concatenateNalDataForFrame = function (frame) { 12042 var i, 12043 currentNal, 12044 dataOffset = 0, 12045 nalsByteLength = frame.byteLength, 12046 numberOfNals = frame.length, 12047 totalByteLength = nalsByteLength + 4 * numberOfNals, 12048 data = new Uint8Array(totalByteLength), 12049 view = new DataView(data.buffer); // For each NAL.. 12050 12051 for (i = 0; i < frame.length; i++) { 12052 currentNal = frame[i]; 12053 view.setUint32(dataOffset, currentNal.data.byteLength); 12054 dataOffset += 4; 12055 data.set(currentNal.data, dataOffset); 12056 dataOffset += currentNal.data.byteLength; 12057 } 12058 12059 return data; 12060 }; 12061 12062 var frameUtils$1 = { 12063 groupNalsIntoFrames: groupNalsIntoFrames, 12064 groupFramesIntoGops: groupFramesIntoGops, 12065 extendFirstKeyFrame: extendFirstKeyFrame, 12066 generateSampleTable: generateSampleTable$1, 12067 concatenateNalData: concatenateNalData, 12068 generateSampleTableForFrame: generateSampleTableForFrame, 12069 concatenateNalDataForFrame: concatenateNalDataForFrame 12070 }; 12071 /** 12072 * mux.js 12073 * 12074 * Copyright (c) Brightcove 12075 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12076 */ 12077 12078 var highPrefix = [33, 16, 5, 32, 164, 27]; 12079 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 12080 12081 var zeroFill = function (count) { 12082 var a = []; 12083 12084 while (count--) { 12085 a.push(0); 12086 } 12087 12088 return a; 12089 }; 12090 12091 var makeTable = function (metaTable) { 12092 return Object.keys(metaTable).reduce(function (obj, key) { 12093 obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) { 12094 return arr.concat(part); 12095 }, [])); 12096 return obj; 12097 }, {}); 12098 }; 12099 12100 var silence; 12101 12102 var silence_1 = function () { 12103 if (!silence) { 12104 // Frames-of-silence to use for filling in missing AAC frames 12105 var coneOfSilence = { 12106 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 12107 88200: [highPrefix, [231], zeroFill(170), [56]], 12108 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 12109 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 12110 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 12111 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 12112 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]], 12113 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]], 12114 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]], 12115 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]], 12116 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]] 12117 }; 12118 silence = makeTable(coneOfSilence); 12119 } 12120 12121 return silence; 12122 }; 12123 /** 12124 * mux.js 12125 * 12126 * Copyright (c) Brightcove 12127 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12128 */ 12129 12130 12131 var ONE_SECOND_IN_TS$4 = 90000, 12132 // 90kHz clock 12133 secondsToVideoTs, 12134 secondsToAudioTs, 12135 videoTsToSeconds, 12136 audioTsToSeconds, 12137 audioTsToVideoTs, 12138 videoTsToAudioTs, 12139 metadataTsToSeconds; 12140 12141 secondsToVideoTs = function (seconds) { 12142 return seconds * ONE_SECOND_IN_TS$4; 12143 }; 12144 12145 secondsToAudioTs = function (seconds, sampleRate) { 12146 return seconds * sampleRate; 12147 }; 12148 12149 videoTsToSeconds = function (timestamp) { 12150 return timestamp / ONE_SECOND_IN_TS$4; 12151 }; 12152 12153 audioTsToSeconds = function (timestamp, sampleRate) { 12154 return timestamp / sampleRate; 12155 }; 12156 12157 audioTsToVideoTs = function (timestamp, sampleRate) { 12158 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 12159 }; 12160 12161 videoTsToAudioTs = function (timestamp, sampleRate) { 12162 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 12163 }; 12164 /** 12165 * Adjust ID3 tag or caption timing information by the timeline pts values 12166 * (if keepOriginalTimestamps is false) and convert to seconds 12167 */ 12168 12169 12170 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 12171 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 12172 }; 12173 12174 var clock$2 = { 12175 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS$4, 12176 secondsToVideoTs: secondsToVideoTs, 12177 secondsToAudioTs: secondsToAudioTs, 12178 videoTsToSeconds: videoTsToSeconds, 12179 audioTsToSeconds: audioTsToSeconds, 12180 audioTsToVideoTs: audioTsToVideoTs, 12181 videoTsToAudioTs: videoTsToAudioTs, 12182 metadataTsToSeconds: metadataTsToSeconds 12183 }; 12184 /** 12185 * mux.js 12186 * 12187 * Copyright (c) Brightcove 12188 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12189 */ 12190 12191 var coneOfSilence = silence_1; 12192 var clock$1 = clock$2; 12193 /** 12194 * Sum the `byteLength` properties of the data in each AAC frame 12195 */ 12196 12197 var sumFrameByteLengths = function (array) { 12198 var i, 12199 currentObj, 12200 sum = 0; // sum the byteLength's all each nal unit in the frame 12201 12202 for (i = 0; i < array.length; i++) { 12203 currentObj = array[i]; 12204 sum += currentObj.data.byteLength; 12205 } 12206 12207 return sum; 12208 }; // Possibly pad (prefix) the audio track with silence if appending this track 12209 // would lead to the introduction of a gap in the audio buffer 12210 12211 12212 var prefixWithSilence = function (track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) { 12213 var baseMediaDecodeTimeTs, 12214 frameDuration = 0, 12215 audioGapDuration = 0, 12216 audioFillFrameCount = 0, 12217 audioFillDuration = 0, 12218 silentFrame, 12219 i, 12220 firstFrame; 12221 12222 if (!frames.length) { 12223 return; 12224 } 12225 12226 baseMediaDecodeTimeTs = clock$1.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills 12227 12228 frameDuration = Math.ceil(clock$1.ONE_SECOND_IN_TS / (track.samplerate / 1024)); 12229 12230 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 12231 // insert the shortest possible amount (audio gap or audio to video gap) 12232 audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap 12233 12234 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 12235 audioFillDuration = audioFillFrameCount * frameDuration; 12236 } // don't attempt to fill gaps smaller than a single frame or larger 12237 // than a half second 12238 12239 12240 if (audioFillFrameCount < 1 || audioFillDuration > clock$1.ONE_SECOND_IN_TS / 2) { 12241 return; 12242 } 12243 12244 silentFrame = coneOfSilence()[track.samplerate]; 12245 12246 if (!silentFrame) { 12247 // we don't have a silent frame pregenerated for the sample rate, so use a frame 12248 // from the content instead 12249 silentFrame = frames[0].data; 12250 } 12251 12252 for (i = 0; i < audioFillFrameCount; i++) { 12253 firstFrame = frames[0]; 12254 frames.splice(0, 0, { 12255 data: silentFrame, 12256 dts: firstFrame.dts - frameDuration, 12257 pts: firstFrame.pts - frameDuration 12258 }); 12259 } 12260 12261 track.baseMediaDecodeTime -= Math.floor(clock$1.videoTsToAudioTs(audioFillDuration, track.samplerate)); 12262 return audioFillDuration; 12263 }; // If the audio segment extends before the earliest allowed dts 12264 // value, remove AAC frames until starts at or after the earliest 12265 // allowed DTS so that we don't end up with a negative baseMedia- 12266 // DecodeTime for the audio track 12267 12268 12269 var trimAdtsFramesByEarliestDts = function (adtsFrames, track, earliestAllowedDts) { 12270 if (track.minSegmentDts >= earliestAllowedDts) { 12271 return adtsFrames; 12272 } // We will need to recalculate the earliest segment Dts 12273 12274 12275 track.minSegmentDts = Infinity; 12276 return adtsFrames.filter(function (currentFrame) { 12277 // If this is an allowed frame, keep it and record it's Dts 12278 if (currentFrame.dts >= earliestAllowedDts) { 12279 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 12280 track.minSegmentPts = track.minSegmentDts; 12281 return true; 12282 } // Otherwise, discard it 12283 12284 12285 return false; 12286 }); 12287 }; // generate the track's raw mdat data from an array of frames 12288 12289 12290 var generateSampleTable = function (frames) { 12291 var i, 12292 currentFrame, 12293 samples = []; 12294 12295 for (i = 0; i < frames.length; i++) { 12296 currentFrame = frames[i]; 12297 samples.push({ 12298 size: currentFrame.data.byteLength, 12299 duration: 1024 // For AAC audio, all samples contain 1024 samples 12300 12301 }); 12302 } 12303 12304 return samples; 12305 }; // generate the track's sample table from an array of frames 12306 12307 12308 var concatenateFrameData = function (frames) { 12309 var i, 12310 currentFrame, 12311 dataOffset = 0, 12312 data = new Uint8Array(sumFrameByteLengths(frames)); 12313 12314 for (i = 0; i < frames.length; i++) { 12315 currentFrame = frames[i]; 12316 data.set(currentFrame.data, dataOffset); 12317 dataOffset += currentFrame.data.byteLength; 12318 } 12319 12320 return data; 12321 }; 12322 12323 var audioFrameUtils$1 = { 12324 prefixWithSilence: prefixWithSilence, 12325 trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts, 12326 generateSampleTable: generateSampleTable, 12327 concatenateFrameData: concatenateFrameData 12328 }; 12329 /** 12330 * mux.js 12331 * 12332 * Copyright (c) Brightcove 12333 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12334 */ 12335 12336 var ONE_SECOND_IN_TS$3 = clock$2.ONE_SECOND_IN_TS; 12337 /** 12338 * Store information about the start and end of the track and the 12339 * duration for each frame/sample we process in order to calculate 12340 * the baseMediaDecodeTime 12341 */ 12342 12343 var collectDtsInfo = function (track, data) { 12344 if (typeof data.pts === 'number') { 12345 if (track.timelineStartInfo.pts === undefined) { 12346 track.timelineStartInfo.pts = data.pts; 12347 } 12348 12349 if (track.minSegmentPts === undefined) { 12350 track.minSegmentPts = data.pts; 12351 } else { 12352 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 12353 } 12354 12355 if (track.maxSegmentPts === undefined) { 12356 track.maxSegmentPts = data.pts; 12357 } else { 12358 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 12359 } 12360 } 12361 12362 if (typeof data.dts === 'number') { 12363 if (track.timelineStartInfo.dts === undefined) { 12364 track.timelineStartInfo.dts = data.dts; 12365 } 12366 12367 if (track.minSegmentDts === undefined) { 12368 track.minSegmentDts = data.dts; 12369 } else { 12370 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 12371 } 12372 12373 if (track.maxSegmentDts === undefined) { 12374 track.maxSegmentDts = data.dts; 12375 } else { 12376 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 12377 } 12378 } 12379 }; 12380 /** 12381 * Clear values used to calculate the baseMediaDecodeTime between 12382 * tracks 12383 */ 12384 12385 12386 var clearDtsInfo = function (track) { 12387 delete track.minSegmentDts; 12388 delete track.maxSegmentDts; 12389 delete track.minSegmentPts; 12390 delete track.maxSegmentPts; 12391 }; 12392 /** 12393 * Calculate the track's baseMediaDecodeTime based on the earliest 12394 * DTS the transmuxer has ever seen and the minimum DTS for the 12395 * current track 12396 * @param track {object} track metadata configuration 12397 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 12398 * in the source; false to adjust the first segment to start at 0. 12399 */ 12400 12401 12402 var calculateTrackBaseMediaDecodeTime = function (track, keepOriginalTimestamps) { 12403 var baseMediaDecodeTime, 12404 scale, 12405 minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero. 12406 12407 if (!keepOriginalTimestamps) { 12408 minSegmentDts -= track.timelineStartInfo.dts; 12409 } // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 12410 // we want the start of the first segment to be placed 12411 12412 12413 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first 12414 12415 baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative 12416 12417 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 12418 12419 if (track.type === 'audio') { 12420 // Audio has a different clock equal to the sampling_rate so we need to 12421 // scale the PTS values into the clock rate of the track 12422 scale = track.samplerate / ONE_SECOND_IN_TS$3; 12423 baseMediaDecodeTime *= scale; 12424 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 12425 } 12426 12427 return baseMediaDecodeTime; 12428 }; 12429 12430 var trackDecodeInfo$1 = { 12431 clearDtsInfo: clearDtsInfo, 12432 calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime, 12433 collectDtsInfo: collectDtsInfo 12434 }; 12435 /** 12436 * mux.js 12437 * 12438 * Copyright (c) Brightcove 12439 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12440 * 12441 * Reads in-band caption information from a video elementary 12442 * stream. Captions must follow the CEA-708 standard for injection 12443 * into an MPEG-2 transport streams. 12444 * @see https://en.wikipedia.org/wiki/CEA-708 12445 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 12446 */ 12447 // payload type field to indicate how they are to be 12448 // interpreted. CEAS-708 caption content is always transmitted with 12449 // payload type 0x04. 12450 12451 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 12452 RBSP_TRAILING_BITS = 128; 12453 /** 12454 * Parse a supplemental enhancement information (SEI) NAL unit. 12455 * Stops parsing once a message of type ITU T T35 has been found. 12456 * 12457 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 12458 * @return {object} the parsed SEI payload 12459 * @see Rec. ITU-T H.264, 7.3.2.3.1 12460 */ 12461 12462 var parseSei = function (bytes) { 12463 var i = 0, 12464 result = { 12465 payloadType: -1, 12466 payloadSize: 0 12467 }, 12468 payloadType = 0, 12469 payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message 12470 12471 while (i < bytes.byteLength) { 12472 // stop once we have hit the end of the sei_rbsp 12473 if (bytes[i] === RBSP_TRAILING_BITS) { 12474 break; 12475 } // Parse payload type 12476 12477 12478 while (bytes[i] === 0xFF) { 12479 payloadType += 255; 12480 i++; 12481 } 12482 12483 payloadType += bytes[i++]; // Parse payload size 12484 12485 while (bytes[i] === 0xFF) { 12486 payloadSize += 255; 12487 i++; 12488 } 12489 12490 payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break 12491 // there can only ever be one caption message in a frame's sei 12492 12493 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 12494 var userIdentifier = String.fromCharCode(bytes[i + 3], bytes[i + 4], bytes[i + 5], bytes[i + 6]); 12495 12496 if (userIdentifier === 'GA94') { 12497 result.payloadType = payloadType; 12498 result.payloadSize = payloadSize; 12499 result.payload = bytes.subarray(i, i + payloadSize); 12500 break; 12501 } else { 12502 result.payload = void 0; 12503 } 12504 } // skip the payload and parse the next message 12505 12506 12507 i += payloadSize; 12508 payloadType = 0; 12509 payloadSize = 0; 12510 } 12511 12512 return result; 12513 }; // see ANSI/SCTE 128-1 (2013), section 8.1 12514 12515 12516 var parseUserData = function (sei) { 12517 // itu_t_t35_contry_code must be 181 (United States) for 12518 // captions 12519 if (sei.payload[0] !== 181) { 12520 return null; 12521 } // itu_t_t35_provider_code should be 49 (ATSC) for captions 12522 12523 12524 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 12525 return null; 12526 } // the user_identifier should be "GA94" to indicate ATSC1 data 12527 12528 12529 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 12530 return null; 12531 } // finally, user_data_type_code should be 0x03 for caption data 12532 12533 12534 if (sei.payload[7] !== 0x03) { 12535 return null; 12536 } // return the user_data_type_structure and strip the trailing 12537 // marker bits 12538 12539 12540 return sei.payload.subarray(8, sei.payload.length - 1); 12541 }; // see CEA-708-D, section 4.4 12542 12543 12544 var parseCaptionPackets = function (pts, userData) { 12545 var results = [], 12546 i, 12547 count, 12548 offset, 12549 data; // if this is just filler, return immediately 12550 12551 if (!(userData[0] & 0x40)) { 12552 return results; 12553 } // parse out the cc_data_1 and cc_data_2 fields 12554 12555 12556 count = userData[0] & 0x1f; 12557 12558 for (i = 0; i < count; i++) { 12559 offset = i * 3; 12560 data = { 12561 type: userData[offset + 2] & 0x03, 12562 pts: pts 12563 }; // capture cc data when cc_valid is 1 12564 12565 if (userData[offset + 2] & 0x04) { 12566 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 12567 results.push(data); 12568 } 12569 } 12570 12571 return results; 12572 }; 12573 12574 var discardEmulationPreventionBytes$1 = function (data) { 12575 var length = data.byteLength, 12576 emulationPreventionBytesPositions = [], 12577 i = 1, 12578 newLength, 12579 newData; // Find all `Emulation Prevention Bytes` 12580 12581 while (i < length - 2) { 12582 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 12583 emulationPreventionBytesPositions.push(i + 2); 12584 i += 2; 12585 } else { 12586 i++; 12587 } 12588 } // If no Emulation Prevention Bytes were found just return the original 12589 // array 12590 12591 12592 if (emulationPreventionBytesPositions.length === 0) { 12593 return data; 12594 } // Create a new array to hold the NAL unit data 12595 12596 12597 newLength = length - emulationPreventionBytesPositions.length; 12598 newData = new Uint8Array(newLength); 12599 var sourceIndex = 0; 12600 12601 for (i = 0; i < newLength; sourceIndex++, i++) { 12602 if (sourceIndex === emulationPreventionBytesPositions[0]) { 12603 // Skip this byte 12604 sourceIndex++; // Remove this position index 12605 12606 emulationPreventionBytesPositions.shift(); 12607 } 12608 12609 newData[i] = data[sourceIndex]; 12610 } 12611 12612 return newData; 12613 }; // exports 12614 12615 12616 var captionPacketParser = { 12617 parseSei: parseSei, 12618 parseUserData: parseUserData, 12619 parseCaptionPackets: parseCaptionPackets, 12620 discardEmulationPreventionBytes: discardEmulationPreventionBytes$1, 12621 USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35 12622 }; 12623 /** 12624 * mux.js 12625 * 12626 * Copyright (c) Brightcove 12627 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12628 * 12629 * Reads in-band caption information from a video elementary 12630 * stream. Captions must follow the CEA-708 standard for injection 12631 * into an MPEG-2 transport streams. 12632 * @see https://en.wikipedia.org/wiki/CEA-708 12633 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 12634 */ 12635 // Link To Transport 12636 // ----------------- 12637 12638 var Stream$7 = stream; 12639 var cea708Parser = captionPacketParser; 12640 12641 var CaptionStream$2 = function (options) { 12642 options = options || {}; 12643 CaptionStream$2.prototype.init.call(this); // parse708captions flag, default to true 12644 12645 this.parse708captions_ = typeof options.parse708captions === 'boolean' ? options.parse708captions : true; 12646 this.captionPackets_ = []; 12647 this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define 12648 new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define 12649 new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define 12650 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 12651 ]; 12652 12653 if (this.parse708captions_) { 12654 this.cc708Stream_ = new Cea708Stream({ 12655 captionServices: options.captionServices 12656 }); // eslint-disable-line no-use-before-define 12657 } 12658 12659 this.reset(); // forward data and done events from CCs to this CaptionStream 12660 12661 this.ccStreams_.forEach(function (cc) { 12662 cc.on('data', this.trigger.bind(this, 'data')); 12663 cc.on('partialdone', this.trigger.bind(this, 'partialdone')); 12664 cc.on('done', this.trigger.bind(this, 'done')); 12665 }, this); 12666 12667 if (this.parse708captions_) { 12668 this.cc708Stream_.on('data', this.trigger.bind(this, 'data')); 12669 this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone')); 12670 this.cc708Stream_.on('done', this.trigger.bind(this, 'done')); 12671 } 12672 }; 12673 12674 CaptionStream$2.prototype = new Stream$7(); 12675 12676 CaptionStream$2.prototype.push = function (event) { 12677 var sei, userData, newCaptionPackets; // only examine SEI NALs 12678 12679 if (event.nalUnitType !== 'sei_rbsp') { 12680 return; 12681 } // parse the sei 12682 12683 12684 sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip 12685 12686 if (!sei.payload) { 12687 return; 12688 } // ignore everything but user_data_registered_itu_t_t35 12689 12690 12691 if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) { 12692 return; 12693 } // parse out the user data payload 12694 12695 12696 userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData 12697 12698 if (!userData) { 12699 return; 12700 } // Sometimes, the same segment # will be downloaded twice. To stop the 12701 // caption data from being processed twice, we track the latest dts we've 12702 // received and ignore everything with a dts before that. However, since 12703 // data for a specific dts can be split across packets on either side of 12704 // a segment boundary, we need to make sure we *don't* ignore the packets 12705 // from the *next* segment that have dts === this.latestDts_. By constantly 12706 // tracking the number of packets received with dts === this.latestDts_, we 12707 // know how many should be ignored once we start receiving duplicates. 12708 12709 12710 if (event.dts < this.latestDts_) { 12711 // We've started getting older data, so set the flag. 12712 this.ignoreNextEqualDts_ = true; 12713 return; 12714 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 12715 this.numSameDts_--; 12716 12717 if (!this.numSameDts_) { 12718 // We've received the last duplicate packet, time to start processing again 12719 this.ignoreNextEqualDts_ = false; 12720 } 12721 12722 return; 12723 } // parse out CC data packets and save them for later 12724 12725 12726 newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData); 12727 this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); 12728 12729 if (this.latestDts_ !== event.dts) { 12730 this.numSameDts_ = 0; 12731 } 12732 12733 this.numSameDts_++; 12734 this.latestDts_ = event.dts; 12735 }; 12736 12737 CaptionStream$2.prototype.flushCCStreams = function (flushType) { 12738 this.ccStreams_.forEach(function (cc) { 12739 return flushType === 'flush' ? cc.flush() : cc.partialFlush(); 12740 }, this); 12741 }; 12742 12743 CaptionStream$2.prototype.flushStream = function (flushType) { 12744 // make sure we actually parsed captions before proceeding 12745 if (!this.captionPackets_.length) { 12746 this.flushCCStreams(flushType); 12747 return; 12748 } // In Chrome, the Array#sort function is not stable so add a 12749 // presortIndex that we can use to ensure we get a stable-sort 12750 12751 12752 this.captionPackets_.forEach(function (elem, idx) { 12753 elem.presortIndex = idx; 12754 }); // sort caption byte-pairs based on their PTS values 12755 12756 this.captionPackets_.sort(function (a, b) { 12757 if (a.pts === b.pts) { 12758 return a.presortIndex - b.presortIndex; 12759 } 12760 12761 return a.pts - b.pts; 12762 }); 12763 this.captionPackets_.forEach(function (packet) { 12764 if (packet.type < 2) { 12765 // Dispatch packet to the right Cea608Stream 12766 this.dispatchCea608Packet(packet); 12767 } else { 12768 // Dispatch packet to the Cea708Stream 12769 this.dispatchCea708Packet(packet); 12770 } 12771 }, this); 12772 this.captionPackets_.length = 0; 12773 this.flushCCStreams(flushType); 12774 }; 12775 12776 CaptionStream$2.prototype.flush = function () { 12777 return this.flushStream('flush'); 12778 }; // Only called if handling partial data 12779 12780 12781 CaptionStream$2.prototype.partialFlush = function () { 12782 return this.flushStream('partialFlush'); 12783 }; 12784 12785 CaptionStream$2.prototype.reset = function () { 12786 this.latestDts_ = null; 12787 this.ignoreNextEqualDts_ = false; 12788 this.numSameDts_ = 0; 12789 this.activeCea608Channel_ = [null, null]; 12790 this.ccStreams_.forEach(function (ccStream) { 12791 ccStream.reset(); 12792 }); 12793 }; // From the CEA-608 spec: 12794 12795 /* 12796 * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed 12797 * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is 12798 * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair 12799 * and subsequent data should then be processed according to the FCC rules. It may be necessary for the 12800 * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) 12801 * to switch to captioning or Text. 12802 */ 12803 // With that in mind, we ignore any data between an XDS control code and a 12804 // subsequent closed-captioning control code. 12805 12806 12807 CaptionStream$2.prototype.dispatchCea608Packet = function (packet) { 12808 // NOTE: packet.type is the CEA608 field 12809 if (this.setsTextOrXDSActive(packet)) { 12810 this.activeCea608Channel_[packet.type] = null; 12811 } else if (this.setsChannel1Active(packet)) { 12812 this.activeCea608Channel_[packet.type] = 0; 12813 } else if (this.setsChannel2Active(packet)) { 12814 this.activeCea608Channel_[packet.type] = 1; 12815 } 12816 12817 if (this.activeCea608Channel_[packet.type] === null) { 12818 // If we haven't received anything to set the active channel, or the 12819 // packets are Text/XDS data, discard the data; we don't want jumbled 12820 // captions 12821 return; 12822 } 12823 12824 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 12825 }; 12826 12827 CaptionStream$2.prototype.setsChannel1Active = function (packet) { 12828 return (packet.ccData & 0x7800) === 0x1000; 12829 }; 12830 12831 CaptionStream$2.prototype.setsChannel2Active = function (packet) { 12832 return (packet.ccData & 0x7800) === 0x1800; 12833 }; 12834 12835 CaptionStream$2.prototype.setsTextOrXDSActive = function (packet) { 12836 return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a; 12837 }; 12838 12839 CaptionStream$2.prototype.dispatchCea708Packet = function (packet) { 12840 if (this.parse708captions_) { 12841 this.cc708Stream_.push(packet); 12842 } 12843 }; // ---------------------- 12844 // Session to Application 12845 // ---------------------- 12846 // This hash maps special and extended character codes to their 12847 // proper Unicode equivalent. The first one-byte key is just a 12848 // non-standard character code. The two-byte keys that follow are 12849 // the extended CEA708 character codes, along with the preceding 12850 // 0x10 extended character byte to distinguish these codes from 12851 // non-extended character codes. Every CEA708 character code that 12852 // is not in this object maps directly to a standard unicode 12853 // character code. 12854 // The transparent space and non-breaking transparent space are 12855 // technically not fully supported since there is no code to 12856 // make them transparent, so they have normal non-transparent 12857 // stand-ins. 12858 // The special closed caption (CC) character isn't a standard 12859 // unicode character, so a fairly similar unicode character was 12860 // chosen in it's place. 12861 12862 12863 var CHARACTER_TRANSLATION_708 = { 12864 0x7f: 0x266a, 12865 // ⪠12866 0x1020: 0x20, 12867 // Transparent Space 12868 0x1021: 0xa0, 12869 // Nob-breaking Transparent Space 12870 0x1025: 0x2026, 12871 // ⦠12872 0x102a: 0x0160, 12873 // Å 12874 0x102c: 0x0152, 12875 // Å 12876 0x1030: 0x2588, 12877 // â 12878 0x1031: 0x2018, 12879 // â 12880 0x1032: 0x2019, 12881 // â 12882 0x1033: 0x201c, 12883 // â 12884 0x1034: 0x201d, 12885 // â 12886 0x1035: 0x2022, 12887 // ⢠12888 0x1039: 0x2122, 12889 // ⢠12890 0x103a: 0x0161, 12891 // Å¡ 12892 0x103c: 0x0153, 12893 // Å 12894 0x103d: 0x2120, 12895 // â 12896 0x103f: 0x0178, 12897 // Ÿ 12898 0x1076: 0x215b, 12899 // â 12900 0x1077: 0x215c, 12901 // â 12902 0x1078: 0x215d, 12903 // â 12904 0x1079: 0x215e, 12905 // â 12906 0x107a: 0x23d0, 12907 // â 12908 0x107b: 0x23a4, 12909 // ⤠12910 0x107c: 0x23a3, 12911 // ⣠12912 0x107d: 0x23af, 12913 // ⯠12914 0x107e: 0x23a6, 12915 // ⦠12916 0x107f: 0x23a1, 12917 // â¡ 12918 0x10a0: 0x3138 // 㸠(CC char) 12919 12920 }; 12921 12922 var get708CharFromCode = function (code) { 12923 var newCode = CHARACTER_TRANSLATION_708[code] || code; 12924 12925 if (code & 0x1000 && code === newCode) { 12926 // Invalid extended code 12927 return ''; 12928 } 12929 12930 return String.fromCharCode(newCode); 12931 }; 12932 12933 var within708TextBlock = function (b) { 12934 return 0x20 <= b && b <= 0x7f || 0xa0 <= b && b <= 0xff; 12935 }; 12936 12937 var Cea708Window = function (windowNum) { 12938 this.windowNum = windowNum; 12939 this.reset(); 12940 }; 12941 12942 Cea708Window.prototype.reset = function () { 12943 this.clearText(); 12944 this.pendingNewLine = false; 12945 this.winAttr = {}; 12946 this.penAttr = {}; 12947 this.penLoc = {}; 12948 this.penColor = {}; // These default values are arbitrary, 12949 // defineWindow will usually override them 12950 12951 this.visible = 0; 12952 this.rowLock = 0; 12953 this.columnLock = 0; 12954 this.priority = 0; 12955 this.relativePositioning = 0; 12956 this.anchorVertical = 0; 12957 this.anchorHorizontal = 0; 12958 this.anchorPoint = 0; 12959 this.rowCount = 1; 12960 this.virtualRowCount = this.rowCount + 1; 12961 this.columnCount = 41; 12962 this.windowStyle = 0; 12963 this.penStyle = 0; 12964 }; 12965 12966 Cea708Window.prototype.getText = function () { 12967 return this.rows.join('\n'); 12968 }; 12969 12970 Cea708Window.prototype.clearText = function () { 12971 this.rows = ['']; 12972 this.rowIdx = 0; 12973 }; 12974 12975 Cea708Window.prototype.newLine = function (pts) { 12976 if (this.rows.length >= this.virtualRowCount && typeof this.beforeRowOverflow === 'function') { 12977 this.beforeRowOverflow(pts); 12978 } 12979 12980 if (this.rows.length > 0) { 12981 this.rows.push(''); 12982 this.rowIdx++; 12983 } // Show all virtual rows since there's no visible scrolling 12984 12985 12986 while (this.rows.length > this.virtualRowCount) { 12987 this.rows.shift(); 12988 this.rowIdx--; 12989 } 12990 }; 12991 12992 Cea708Window.prototype.isEmpty = function () { 12993 if (this.rows.length === 0) { 12994 return true; 12995 } else if (this.rows.length === 1) { 12996 return this.rows[0] === ''; 12997 } 12998 12999 return false; 13000 }; 13001 13002 Cea708Window.prototype.addText = function (text) { 13003 this.rows[this.rowIdx] += text; 13004 }; 13005 13006 Cea708Window.prototype.backspace = function () { 13007 if (!this.isEmpty()) { 13008 var row = this.rows[this.rowIdx]; 13009 this.rows[this.rowIdx] = row.substr(0, row.length - 1); 13010 } 13011 }; 13012 13013 var Cea708Service = function (serviceNum, encoding, stream) { 13014 this.serviceNum = serviceNum; 13015 this.text = ''; 13016 this.currentWindow = new Cea708Window(-1); 13017 this.windows = []; 13018 this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided 13019 13020 if (typeof encoding === 'string') { 13021 this.createTextDecoder(encoding); 13022 } 13023 }; 13024 /** 13025 * Initialize service windows 13026 * Must be run before service use 13027 * 13028 * @param {Integer} pts PTS value 13029 * @param {Function} beforeRowOverflow Function to execute before row overflow of a window 13030 */ 13031 13032 13033 Cea708Service.prototype.init = function (pts, beforeRowOverflow) { 13034 this.startPts = pts; 13035 13036 for (var win = 0; win < 8; win++) { 13037 this.windows[win] = new Cea708Window(win); 13038 13039 if (typeof beforeRowOverflow === 'function') { 13040 this.windows[win].beforeRowOverflow = beforeRowOverflow; 13041 } 13042 } 13043 }; 13044 /** 13045 * Set current window of service to be affected by commands 13046 * 13047 * @param {Integer} windowNum Window number 13048 */ 13049 13050 13051 Cea708Service.prototype.setCurrentWindow = function (windowNum) { 13052 this.currentWindow = this.windows[windowNum]; 13053 }; 13054 /** 13055 * Try to create a TextDecoder if it is natively supported 13056 */ 13057 13058 13059 Cea708Service.prototype.createTextDecoder = function (encoding) { 13060 if (typeof TextDecoder === 'undefined') { 13061 this.stream.trigger('log', { 13062 level: 'warn', 13063 message: 'The `encoding` option is unsupported without TextDecoder support' 13064 }); 13065 } else { 13066 try { 13067 this.textDecoder_ = new TextDecoder(encoding); 13068 } catch (error) { 13069 this.stream.trigger('log', { 13070 level: 'warn', 13071 message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error 13072 }); 13073 } 13074 } 13075 }; 13076 13077 var Cea708Stream = function (options) { 13078 options = options || {}; 13079 Cea708Stream.prototype.init.call(this); 13080 var self = this; 13081 var captionServices = options.captionServices || {}; 13082 var captionServiceEncodings = {}; 13083 var serviceProps; // Get service encodings from captionServices option block 13084 13085 Object.keys(captionServices).forEach(serviceName => { 13086 serviceProps = captionServices[serviceName]; 13087 13088 if (/^SERVICE/.test(serviceName)) { 13089 captionServiceEncodings[serviceName] = serviceProps.encoding; 13090 } 13091 }); 13092 this.serviceEncodings = captionServiceEncodings; 13093 this.current708Packet = null; 13094 this.services = {}; 13095 13096 this.push = function (packet) { 13097 if (packet.type === 3) { 13098 // 708 packet start 13099 self.new708Packet(); 13100 self.add708Bytes(packet); 13101 } else { 13102 if (self.current708Packet === null) { 13103 // This should only happen at the start of a file if there's no packet start. 13104 self.new708Packet(); 13105 } 13106 13107 self.add708Bytes(packet); 13108 } 13109 }; 13110 }; 13111 13112 Cea708Stream.prototype = new Stream$7(); 13113 /** 13114 * Push current 708 packet, create new 708 packet. 13115 */ 13116 13117 Cea708Stream.prototype.new708Packet = function () { 13118 if (this.current708Packet !== null) { 13119 this.push708Packet(); 13120 } 13121 13122 this.current708Packet = { 13123 data: [], 13124 ptsVals: [] 13125 }; 13126 }; 13127 /** 13128 * Add pts and both bytes from packet into current 708 packet. 13129 */ 13130 13131 13132 Cea708Stream.prototype.add708Bytes = function (packet) { 13133 var data = packet.ccData; 13134 var byte0 = data >>> 8; 13135 var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec 13136 // that service blocks will always line up with byte pairs. 13137 13138 this.current708Packet.ptsVals.push(packet.pts); 13139 this.current708Packet.data.push(byte0); 13140 this.current708Packet.data.push(byte1); 13141 }; 13142 /** 13143 * Parse completed 708 packet into service blocks and push each service block. 13144 */ 13145 13146 13147 Cea708Stream.prototype.push708Packet = function () { 13148 var packet708 = this.current708Packet; 13149 var packetData = packet708.data; 13150 var serviceNum = null; 13151 var blockSize = null; 13152 var i = 0; 13153 var b = packetData[i++]; 13154 packet708.seq = b >> 6; 13155 packet708.sizeCode = b & 0x3f; // 0b00111111; 13156 13157 for (; i < packetData.length; i++) { 13158 b = packetData[i++]; 13159 serviceNum = b >> 5; 13160 blockSize = b & 0x1f; // 0b00011111 13161 13162 if (serviceNum === 7 && blockSize > 0) { 13163 // Extended service num 13164 b = packetData[i++]; 13165 serviceNum = b; 13166 } 13167 13168 this.pushServiceBlock(serviceNum, i, blockSize); 13169 13170 if (blockSize > 0) { 13171 i += blockSize - 1; 13172 } 13173 } 13174 }; 13175 /** 13176 * Parse service block, execute commands, read text. 13177 * 13178 * Note: While many of these commands serve important purposes, 13179 * many others just parse out the parameters or attributes, but 13180 * nothing is done with them because this is not a full and complete 13181 * implementation of the entire 708 spec. 13182 * 13183 * @param {Integer} serviceNum Service number 13184 * @param {Integer} start Start index of the 708 packet data 13185 * @param {Integer} size Block size 13186 */ 13187 13188 13189 Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) { 13190 var b; 13191 var i = start; 13192 var packetData = this.current708Packet.data; 13193 var service = this.services[serviceNum]; 13194 13195 if (!service) { 13196 service = this.initService(serviceNum, i); 13197 } 13198 13199 for (; i < start + size && i < packetData.length; i++) { 13200 b = packetData[i]; 13201 13202 if (within708TextBlock(b)) { 13203 i = this.handleText(i, service); 13204 } else if (b === 0x18) { 13205 i = this.multiByteCharacter(i, service); 13206 } else if (b === 0x10) { 13207 i = this.extendedCommands(i, service); 13208 } else if (0x80 <= b && b <= 0x87) { 13209 i = this.setCurrentWindow(i, service); 13210 } else if (0x98 <= b && b <= 0x9f) { 13211 i = this.defineWindow(i, service); 13212 } else if (b === 0x88) { 13213 i = this.clearWindows(i, service); 13214 } else if (b === 0x8c) { 13215 i = this.deleteWindows(i, service); 13216 } else if (b === 0x89) { 13217 i = this.displayWindows(i, service); 13218 } else if (b === 0x8a) { 13219 i = this.hideWindows(i, service); 13220 } else if (b === 0x8b) { 13221 i = this.toggleWindows(i, service); 13222 } else if (b === 0x97) { 13223 i = this.setWindowAttributes(i, service); 13224 } else if (b === 0x90) { 13225 i = this.setPenAttributes(i, service); 13226 } else if (b === 0x91) { 13227 i = this.setPenColor(i, service); 13228 } else if (b === 0x92) { 13229 i = this.setPenLocation(i, service); 13230 } else if (b === 0x8f) { 13231 service = this.reset(i, service); 13232 } else if (b === 0x08) { 13233 // BS: Backspace 13234 service.currentWindow.backspace(); 13235 } else if (b === 0x0c) { 13236 // FF: Form feed 13237 service.currentWindow.clearText(); 13238 } else if (b === 0x0d) { 13239 // CR: Carriage return 13240 service.currentWindow.pendingNewLine = true; 13241 } else if (b === 0x0e) { 13242 // HCR: Horizontal carriage return 13243 service.currentWindow.clearText(); 13244 } else if (b === 0x8d) { 13245 // DLY: Delay, nothing to do 13246 i++; 13247 } else ; 13248 } 13249 }; 13250 /** 13251 * Execute an extended command 13252 * 13253 * @param {Integer} i Current index in the 708 packet 13254 * @param {Service} service The service object to be affected 13255 * @return {Integer} New index after parsing 13256 */ 13257 13258 13259 Cea708Stream.prototype.extendedCommands = function (i, service) { 13260 var packetData = this.current708Packet.data; 13261 var b = packetData[++i]; 13262 13263 if (within708TextBlock(b)) { 13264 i = this.handleText(i, service, { 13265 isExtended: true 13266 }); 13267 } 13268 13269 return i; 13270 }; 13271 /** 13272 * Get PTS value of a given byte index 13273 * 13274 * @param {Integer} byteIndex Index of the byte 13275 * @return {Integer} PTS 13276 */ 13277 13278 13279 Cea708Stream.prototype.getPts = function (byteIndex) { 13280 // There's 1 pts value per 2 bytes 13281 return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)]; 13282 }; 13283 /** 13284 * Initializes a service 13285 * 13286 * @param {Integer} serviceNum Service number 13287 * @return {Service} Initialized service object 13288 */ 13289 13290 13291 Cea708Stream.prototype.initService = function (serviceNum, i) { 13292 var serviceName = 'SERVICE' + serviceNum; 13293 var self = this; 13294 var serviceName; 13295 var encoding; 13296 13297 if (serviceName in this.serviceEncodings) { 13298 encoding = this.serviceEncodings[serviceName]; 13299 } 13300 13301 this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self); 13302 this.services[serviceNum].init(this.getPts(i), function (pts) { 13303 self.flushDisplayed(pts, self.services[serviceNum]); 13304 }); 13305 return this.services[serviceNum]; 13306 }; 13307 /** 13308 * Execute text writing to current window 13309 * 13310 * @param {Integer} i Current index in the 708 packet 13311 * @param {Service} service The service object to be affected 13312 * @return {Integer} New index after parsing 13313 */ 13314 13315 13316 Cea708Stream.prototype.handleText = function (i, service, options) { 13317 var isExtended = options && options.isExtended; 13318 var isMultiByte = options && options.isMultiByte; 13319 var packetData = this.current708Packet.data; 13320 var extended = isExtended ? 0x1000 : 0x0000; 13321 var currentByte = packetData[i]; 13322 var nextByte = packetData[i + 1]; 13323 var win = service.currentWindow; 13324 var char; 13325 var charCodeArray; // Converts an array of bytes to a unicode hex string. 13326 13327 function toHexString(byteArray) { 13328 return byteArray.map(byte => { 13329 return ('0' + (byte & 0xFF).toString(16)).slice(-2); 13330 }).join(''); 13331 } 13332 13333 if (isMultiByte) { 13334 charCodeArray = [currentByte, nextByte]; 13335 i++; 13336 } else { 13337 charCodeArray = [currentByte]; 13338 } // Use the TextDecoder if one was created for this service 13339 13340 13341 if (service.textDecoder_ && !isExtended) { 13342 char = service.textDecoder_.decode(new Uint8Array(charCodeArray)); 13343 } else { 13344 // We assume any multi-byte char without a decoder is unicode. 13345 if (isMultiByte) { 13346 const unicode = toHexString(charCodeArray); // Takes a unicode hex string and creates a single character. 13347 13348 char = String.fromCharCode(parseInt(unicode, 16)); 13349 } else { 13350 char = get708CharFromCode(extended | currentByte); 13351 } 13352 } 13353 13354 if (win.pendingNewLine && !win.isEmpty()) { 13355 win.newLine(this.getPts(i)); 13356 } 13357 13358 win.pendingNewLine = false; 13359 win.addText(char); 13360 return i; 13361 }; 13362 /** 13363 * Handle decoding of multibyte character 13364 * 13365 * @param {Integer} i Current index in the 708 packet 13366 * @param {Service} service The service object to be affected 13367 * @return {Integer} New index after parsing 13368 */ 13369 13370 13371 Cea708Stream.prototype.multiByteCharacter = function (i, service) { 13372 var packetData = this.current708Packet.data; 13373 var firstByte = packetData[i + 1]; 13374 var secondByte = packetData[i + 2]; 13375 13376 if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) { 13377 i = this.handleText(++i, service, { 13378 isMultiByte: true 13379 }); 13380 } 13381 13382 return i; 13383 }; 13384 /** 13385 * Parse and execute the CW# command. 13386 * 13387 * Set the current window. 13388 * 13389 * @param {Integer} i Current index in the 708 packet 13390 * @param {Service} service The service object to be affected 13391 * @return {Integer} New index after parsing 13392 */ 13393 13394 13395 Cea708Stream.prototype.setCurrentWindow = function (i, service) { 13396 var packetData = this.current708Packet.data; 13397 var b = packetData[i]; 13398 var windowNum = b & 0x07; 13399 service.setCurrentWindow(windowNum); 13400 return i; 13401 }; 13402 /** 13403 * Parse and execute the DF# command. 13404 * 13405 * Define a window and set it as the current window. 13406 * 13407 * @param {Integer} i Current index in the 708 packet 13408 * @param {Service} service The service object to be affected 13409 * @return {Integer} New index after parsing 13410 */ 13411 13412 13413 Cea708Stream.prototype.defineWindow = function (i, service) { 13414 var packetData = this.current708Packet.data; 13415 var b = packetData[i]; 13416 var windowNum = b & 0x07; 13417 service.setCurrentWindow(windowNum); 13418 var win = service.currentWindow; 13419 b = packetData[++i]; 13420 win.visible = (b & 0x20) >> 5; // v 13421 13422 win.rowLock = (b & 0x10) >> 4; // rl 13423 13424 win.columnLock = (b & 0x08) >> 3; // cl 13425 13426 win.priority = b & 0x07; // p 13427 13428 b = packetData[++i]; 13429 win.relativePositioning = (b & 0x80) >> 7; // rp 13430 13431 win.anchorVertical = b & 0x7f; // av 13432 13433 b = packetData[++i]; 13434 win.anchorHorizontal = b; // ah 13435 13436 b = packetData[++i]; 13437 win.anchorPoint = (b & 0xf0) >> 4; // ap 13438 13439 win.rowCount = b & 0x0f; // rc 13440 13441 b = packetData[++i]; 13442 win.columnCount = b & 0x3f; // cc 13443 13444 b = packetData[++i]; 13445 win.windowStyle = (b & 0x38) >> 3; // ws 13446 13447 win.penStyle = b & 0x07; // ps 13448 // The spec says there are (rowCount+1) "virtual rows" 13449 13450 win.virtualRowCount = win.rowCount + 1; 13451 return i; 13452 }; 13453 /** 13454 * Parse and execute the SWA command. 13455 * 13456 * Set attributes of the current window. 13457 * 13458 * @param {Integer} i Current index in the 708 packet 13459 * @param {Service} service The service object to be affected 13460 * @return {Integer} New index after parsing 13461 */ 13462 13463 13464 Cea708Stream.prototype.setWindowAttributes = function (i, service) { 13465 var packetData = this.current708Packet.data; 13466 var b = packetData[i]; 13467 var winAttr = service.currentWindow.winAttr; 13468 b = packetData[++i]; 13469 winAttr.fillOpacity = (b & 0xc0) >> 6; // fo 13470 13471 winAttr.fillRed = (b & 0x30) >> 4; // fr 13472 13473 winAttr.fillGreen = (b & 0x0c) >> 2; // fg 13474 13475 winAttr.fillBlue = b & 0x03; // fb 13476 13477 b = packetData[++i]; 13478 winAttr.borderType = (b & 0xc0) >> 6; // bt 13479 13480 winAttr.borderRed = (b & 0x30) >> 4; // br 13481 13482 winAttr.borderGreen = (b & 0x0c) >> 2; // bg 13483 13484 winAttr.borderBlue = b & 0x03; // bb 13485 13486 b = packetData[++i]; 13487 winAttr.borderType += (b & 0x80) >> 5; // bt 13488 13489 winAttr.wordWrap = (b & 0x40) >> 6; // ww 13490 13491 winAttr.printDirection = (b & 0x30) >> 4; // pd 13492 13493 winAttr.scrollDirection = (b & 0x0c) >> 2; // sd 13494 13495 winAttr.justify = b & 0x03; // j 13496 13497 b = packetData[++i]; 13498 winAttr.effectSpeed = (b & 0xf0) >> 4; // es 13499 13500 winAttr.effectDirection = (b & 0x0c) >> 2; // ed 13501 13502 winAttr.displayEffect = b & 0x03; // de 13503 13504 return i; 13505 }; 13506 /** 13507 * Gather text from all displayed windows and push a caption to output. 13508 * 13509 * @param {Integer} i Current index in the 708 packet 13510 * @param {Service} service The service object to be affected 13511 */ 13512 13513 13514 Cea708Stream.prototype.flushDisplayed = function (pts, service) { 13515 var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily 13516 // display text in the correct order, but sample files so far have not shown any issue. 13517 13518 for (var winId = 0; winId < 8; winId++) { 13519 if (service.windows[winId].visible && !service.windows[winId].isEmpty()) { 13520 displayedText.push(service.windows[winId].getText()); 13521 } 13522 } 13523 13524 service.endPts = pts; 13525 service.text = displayedText.join('\n\n'); 13526 this.pushCaption(service); 13527 service.startPts = pts; 13528 }; 13529 /** 13530 * Push a caption to output if the caption contains text. 13531 * 13532 * @param {Service} service The service object to be affected 13533 */ 13534 13535 13536 Cea708Stream.prototype.pushCaption = function (service) { 13537 if (service.text !== '') { 13538 this.trigger('data', { 13539 startPts: service.startPts, 13540 endPts: service.endPts, 13541 text: service.text, 13542 stream: 'cc708_' + service.serviceNum 13543 }); 13544 service.text = ''; 13545 service.startPts = service.endPts; 13546 } 13547 }; 13548 /** 13549 * Parse and execute the DSW command. 13550 * 13551 * Set visible property of windows based on the parsed bitmask. 13552 * 13553 * @param {Integer} i Current index in the 708 packet 13554 * @param {Service} service The service object to be affected 13555 * @return {Integer} New index after parsing 13556 */ 13557 13558 13559 Cea708Stream.prototype.displayWindows = function (i, service) { 13560 var packetData = this.current708Packet.data; 13561 var b = packetData[++i]; 13562 var pts = this.getPts(i); 13563 this.flushDisplayed(pts, service); 13564 13565 for (var winId = 0; winId < 8; winId++) { 13566 if (b & 0x01 << winId) { 13567 service.windows[winId].visible = 1; 13568 } 13569 } 13570 13571 return i; 13572 }; 13573 /** 13574 * Parse and execute the HDW command. 13575 * 13576 * Set visible property of windows based on the parsed bitmask. 13577 * 13578 * @param {Integer} i Current index in the 708 packet 13579 * @param {Service} service The service object to be affected 13580 * @return {Integer} New index after parsing 13581 */ 13582 13583 13584 Cea708Stream.prototype.hideWindows = function (i, service) { 13585 var packetData = this.current708Packet.data; 13586 var b = packetData[++i]; 13587 var pts = this.getPts(i); 13588 this.flushDisplayed(pts, service); 13589 13590 for (var winId = 0; winId < 8; winId++) { 13591 if (b & 0x01 << winId) { 13592 service.windows[winId].visible = 0; 13593 } 13594 } 13595 13596 return i; 13597 }; 13598 /** 13599 * Parse and execute the TGW command. 13600 * 13601 * Set visible property of windows based on the parsed bitmask. 13602 * 13603 * @param {Integer} i Current index in the 708 packet 13604 * @param {Service} service The service object to be affected 13605 * @return {Integer} New index after parsing 13606 */ 13607 13608 13609 Cea708Stream.prototype.toggleWindows = function (i, service) { 13610 var packetData = this.current708Packet.data; 13611 var b = packetData[++i]; 13612 var pts = this.getPts(i); 13613 this.flushDisplayed(pts, service); 13614 13615 for (var winId = 0; winId < 8; winId++) { 13616 if (b & 0x01 << winId) { 13617 service.windows[winId].visible ^= 1; 13618 } 13619 } 13620 13621 return i; 13622 }; 13623 /** 13624 * Parse and execute the CLW command. 13625 * 13626 * Clear text of windows based on the parsed bitmask. 13627 * 13628 * @param {Integer} i Current index in the 708 packet 13629 * @param {Service} service The service object to be affected 13630 * @return {Integer} New index after parsing 13631 */ 13632 13633 13634 Cea708Stream.prototype.clearWindows = function (i, service) { 13635 var packetData = this.current708Packet.data; 13636 var b = packetData[++i]; 13637 var pts = this.getPts(i); 13638 this.flushDisplayed(pts, service); 13639 13640 for (var winId = 0; winId < 8; winId++) { 13641 if (b & 0x01 << winId) { 13642 service.windows[winId].clearText(); 13643 } 13644 } 13645 13646 return i; 13647 }; 13648 /** 13649 * Parse and execute the DLW command. 13650 * 13651 * Re-initialize windows based on the parsed bitmask. 13652 * 13653 * @param {Integer} i Current index in the 708 packet 13654 * @param {Service} service The service object to be affected 13655 * @return {Integer} New index after parsing 13656 */ 13657 13658 13659 Cea708Stream.prototype.deleteWindows = function (i, service) { 13660 var packetData = this.current708Packet.data; 13661 var b = packetData[++i]; 13662 var pts = this.getPts(i); 13663 this.flushDisplayed(pts, service); 13664 13665 for (var winId = 0; winId < 8; winId++) { 13666 if (b & 0x01 << winId) { 13667 service.windows[winId].reset(); 13668 } 13669 } 13670 13671 return i; 13672 }; 13673 /** 13674 * Parse and execute the SPA command. 13675 * 13676 * Set pen attributes of the current window. 13677 * 13678 * @param {Integer} i Current index in the 708 packet 13679 * @param {Service} service The service object to be affected 13680 * @return {Integer} New index after parsing 13681 */ 13682 13683 13684 Cea708Stream.prototype.setPenAttributes = function (i, service) { 13685 var packetData = this.current708Packet.data; 13686 var b = packetData[i]; 13687 var penAttr = service.currentWindow.penAttr; 13688 b = packetData[++i]; 13689 penAttr.textTag = (b & 0xf0) >> 4; // tt 13690 13691 penAttr.offset = (b & 0x0c) >> 2; // o 13692 13693 penAttr.penSize = b & 0x03; // s 13694 13695 b = packetData[++i]; 13696 penAttr.italics = (b & 0x80) >> 7; // i 13697 13698 penAttr.underline = (b & 0x40) >> 6; // u 13699 13700 penAttr.edgeType = (b & 0x38) >> 3; // et 13701 13702 penAttr.fontStyle = b & 0x07; // fs 13703 13704 return i; 13705 }; 13706 /** 13707 * Parse and execute the SPC command. 13708 * 13709 * Set pen color of the current window. 13710 * 13711 * @param {Integer} i Current index in the 708 packet 13712 * @param {Service} service The service object to be affected 13713 * @return {Integer} New index after parsing 13714 */ 13715 13716 13717 Cea708Stream.prototype.setPenColor = function (i, service) { 13718 var packetData = this.current708Packet.data; 13719 var b = packetData[i]; 13720 var penColor = service.currentWindow.penColor; 13721 b = packetData[++i]; 13722 penColor.fgOpacity = (b & 0xc0) >> 6; // fo 13723 13724 penColor.fgRed = (b & 0x30) >> 4; // fr 13725 13726 penColor.fgGreen = (b & 0x0c) >> 2; // fg 13727 13728 penColor.fgBlue = b & 0x03; // fb 13729 13730 b = packetData[++i]; 13731 penColor.bgOpacity = (b & 0xc0) >> 6; // bo 13732 13733 penColor.bgRed = (b & 0x30) >> 4; // br 13734 13735 penColor.bgGreen = (b & 0x0c) >> 2; // bg 13736 13737 penColor.bgBlue = b & 0x03; // bb 13738 13739 b = packetData[++i]; 13740 penColor.edgeRed = (b & 0x30) >> 4; // er 13741 13742 penColor.edgeGreen = (b & 0x0c) >> 2; // eg 13743 13744 penColor.edgeBlue = b & 0x03; // eb 13745 13746 return i; 13747 }; 13748 /** 13749 * Parse and execute the SPL command. 13750 * 13751 * Set pen location of the current window. 13752 * 13753 * @param {Integer} i Current index in the 708 packet 13754 * @param {Service} service The service object to be affected 13755 * @return {Integer} New index after parsing 13756 */ 13757 13758 13759 Cea708Stream.prototype.setPenLocation = function (i, service) { 13760 var packetData = this.current708Packet.data; 13761 var b = packetData[i]; 13762 var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak 13763 13764 service.currentWindow.pendingNewLine = true; 13765 b = packetData[++i]; 13766 penLoc.row = b & 0x0f; // r 13767 13768 b = packetData[++i]; 13769 penLoc.column = b & 0x3f; // c 13770 13771 return i; 13772 }; 13773 /** 13774 * Execute the RST command. 13775 * 13776 * Reset service to a clean slate. Re-initialize. 13777 * 13778 * @param {Integer} i Current index in the 708 packet 13779 * @param {Service} service The service object to be affected 13780 * @return {Service} Re-initialized service 13781 */ 13782 13783 13784 Cea708Stream.prototype.reset = function (i, service) { 13785 var pts = this.getPts(i); 13786 this.flushDisplayed(pts, service); 13787 return this.initService(service.serviceNum, i); 13788 }; // This hash maps non-ASCII, special, and extended character codes to their 13789 // proper Unicode equivalent. The first keys that are only a single byte 13790 // are the non-standard ASCII characters, which simply map the CEA608 byte 13791 // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 13792 // character codes, but have their MSB bitmasked with 0x03 so that a lookup 13793 // can be performed regardless of the field and data channel on which the 13794 // character code was received. 13795 13796 13797 var CHARACTER_TRANSLATION = { 13798 0x2a: 0xe1, 13799 // á 13800 0x5c: 0xe9, 13801 // é 13802 0x5e: 0xed, 13803 // à 13804 0x5f: 0xf3, 13805 // ó 13806 0x60: 0xfa, 13807 // ú 13808 0x7b: 0xe7, 13809 // ç 13810 0x7c: 0xf7, 13811 // ÷ 13812 0x7d: 0xd1, 13813 // à 13814 0x7e: 0xf1, 13815 // ñ 13816 0x7f: 0x2588, 13817 // â 13818 0x0130: 0xae, 13819 // ® 13820 0x0131: 0xb0, 13821 // ° 13822 0x0132: 0xbd, 13823 // ½ 13824 0x0133: 0xbf, 13825 // ¿ 13826 0x0134: 0x2122, 13827 // ⢠13828 0x0135: 0xa2, 13829 // ¢ 13830 0x0136: 0xa3, 13831 // £ 13832 0x0137: 0x266a, 13833 // ⪠13834 0x0138: 0xe0, 13835 // à 13836 0x0139: 0xa0, 13837 // 13838 0x013a: 0xe8, 13839 // è 13840 0x013b: 0xe2, 13841 // â 13842 0x013c: 0xea, 13843 // ê 13844 0x013d: 0xee, 13845 // î 13846 0x013e: 0xf4, 13847 // ô 13848 0x013f: 0xfb, 13849 // û 13850 0x0220: 0xc1, 13851 // à 13852 0x0221: 0xc9, 13853 // à 13854 0x0222: 0xd3, 13855 // à 13856 0x0223: 0xda, 13857 // à 13858 0x0224: 0xdc, 13859 // à 13860 0x0225: 0xfc, 13861 // ü 13862 0x0226: 0x2018, 13863 // â 13864 0x0227: 0xa1, 13865 // ¡ 13866 0x0228: 0x2a, 13867 // * 13868 0x0229: 0x27, 13869 // ' 13870 0x022a: 0x2014, 13871 // â 13872 0x022b: 0xa9, 13873 // © 13874 0x022c: 0x2120, 13875 // â 13876 0x022d: 0x2022, 13877 // ⢠13878 0x022e: 0x201c, 13879 // â 13880 0x022f: 0x201d, 13881 // â 13882 0x0230: 0xc0, 13883 // à 13884 0x0231: 0xc2, 13885 // à 13886 0x0232: 0xc7, 13887 // à 13888 0x0233: 0xc8, 13889 // à 13890 0x0234: 0xca, 13891 // à 13892 0x0235: 0xcb, 13893 // à 13894 0x0236: 0xeb, 13895 // ë 13896 0x0237: 0xce, 13897 // à 13898 0x0238: 0xcf, 13899 // à 13900 0x0239: 0xef, 13901 // ï 13902 0x023a: 0xd4, 13903 // à 13904 0x023b: 0xd9, 13905 // à 13906 0x023c: 0xf9, 13907 // ù 13908 0x023d: 0xdb, 13909 // à 13910 0x023e: 0xab, 13911 // « 13912 0x023f: 0xbb, 13913 // » 13914 0x0320: 0xc3, 13915 // à 13916 0x0321: 0xe3, 13917 // ã 13918 0x0322: 0xcd, 13919 // à 13920 0x0323: 0xcc, 13921 // à 13922 0x0324: 0xec, 13923 // ì 13924 0x0325: 0xd2, 13925 // à 13926 0x0326: 0xf2, 13927 // ò 13928 0x0327: 0xd5, 13929 // à 13930 0x0328: 0xf5, 13931 // õ 13932 0x0329: 0x7b, 13933 // { 13934 0x032a: 0x7d, 13935 // } 13936 0x032b: 0x5c, 13937 // \ 13938 0x032c: 0x5e, 13939 // ^ 13940 0x032d: 0x5f, 13941 // _ 13942 0x032e: 0x7c, 13943 // | 13944 0x032f: 0x7e, 13945 // ~ 13946 0x0330: 0xc4, 13947 // à 13948 0x0331: 0xe4, 13949 // ä 13950 0x0332: 0xd6, 13951 // à 13952 0x0333: 0xf6, 13953 // ö 13954 0x0334: 0xdf, 13955 // à 13956 0x0335: 0xa5, 13957 // Â¥ 13958 0x0336: 0xa4, 13959 // ¤ 13960 0x0337: 0x2502, 13961 // â 13962 0x0338: 0xc5, 13963 // à 13964 0x0339: 0xe5, 13965 // Ã¥ 13966 0x033a: 0xd8, 13967 // à 13968 0x033b: 0xf8, 13969 // ø 13970 0x033c: 0x250c, 13971 // â 13972 0x033d: 0x2510, 13973 // â 13974 0x033e: 0x2514, 13975 // â 13976 0x033f: 0x2518 // â 13977 13978 }; 13979 13980 var getCharFromCode = function (code) { 13981 if (code === null) { 13982 return ''; 13983 } 13984 13985 code = CHARACTER_TRANSLATION[code] || code; 13986 return String.fromCharCode(code); 13987 }; // the index of the last row in a CEA-608 display buffer 13988 13989 13990 var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of 13991 // getting it through bit logic. 13992 13993 var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; // CEA-608 captions are rendered onto a 34x15 matrix of character 13994 // cells. The "bottom" row is the last element in the outer array. 13995 // We keep track of positioning information as we go by storing the 13996 // number of indentations and the tab offset in this buffer. 13997 13998 var createDisplayBuffer = function () { 13999 var result = [], 14000 i = BOTTOM_ROW + 1; 14001 14002 while (i--) { 14003 result.push({ 14004 text: '', 14005 indent: 0, 14006 offset: 0 14007 }); 14008 } 14009 14010 return result; 14011 }; 14012 14013 var Cea608Stream = function (field, dataChannel) { 14014 Cea608Stream.prototype.init.call(this); 14015 this.field_ = field || 0; 14016 this.dataChannel_ = dataChannel || 0; 14017 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 14018 this.setConstants(); 14019 this.reset(); 14020 14021 this.push = function (packet) { 14022 var data, swap, char0, char1, text; // remove the parity bits 14023 14024 data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice 14025 14026 if (data === this.lastControlCode_) { 14027 this.lastControlCode_ = null; 14028 return; 14029 } // Store control codes 14030 14031 14032 if ((data & 0xf000) === 0x1000) { 14033 this.lastControlCode_ = data; 14034 } else if (data !== this.PADDING_) { 14035 this.lastControlCode_ = null; 14036 } 14037 14038 char0 = data >>> 8; 14039 char1 = data & 0xff; 14040 14041 if (data === this.PADDING_) { 14042 return; 14043 } else if (data === this.RESUME_CAPTION_LOADING_) { 14044 this.mode_ = 'popOn'; 14045 } else if (data === this.END_OF_CAPTION_) { 14046 // If an EOC is received while in paint-on mode, the displayed caption 14047 // text should be swapped to non-displayed memory as if it was a pop-on 14048 // caption. Because of that, we should explicitly switch back to pop-on 14049 // mode 14050 this.mode_ = 'popOn'; 14051 this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now 14052 14053 this.flushDisplayed(packet.pts); // flip memory 14054 14055 swap = this.displayed_; 14056 this.displayed_ = this.nonDisplayed_; 14057 this.nonDisplayed_ = swap; // start measuring the time to display the caption 14058 14059 this.startPts_ = packet.pts; 14060 } else if (data === this.ROLL_UP_2_ROWS_) { 14061 this.rollUpRows_ = 2; 14062 this.setRollUp(packet.pts); 14063 } else if (data === this.ROLL_UP_3_ROWS_) { 14064 this.rollUpRows_ = 3; 14065 this.setRollUp(packet.pts); 14066 } else if (data === this.ROLL_UP_4_ROWS_) { 14067 this.rollUpRows_ = 4; 14068 this.setRollUp(packet.pts); 14069 } else if (data === this.CARRIAGE_RETURN_) { 14070 this.clearFormatting(packet.pts); 14071 this.flushDisplayed(packet.pts); 14072 this.shiftRowsUp_(); 14073 this.startPts_ = packet.pts; 14074 } else if (data === this.BACKSPACE_) { 14075 if (this.mode_ === 'popOn') { 14076 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 14077 } else { 14078 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 14079 } 14080 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 14081 this.flushDisplayed(packet.pts); 14082 this.displayed_ = createDisplayBuffer(); 14083 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 14084 this.nonDisplayed_ = createDisplayBuffer(); 14085 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 14086 if (this.mode_ !== 'paintOn') { 14087 // NOTE: This should be removed when proper caption positioning is 14088 // implemented 14089 this.flushDisplayed(packet.pts); 14090 this.displayed_ = createDisplayBuffer(); 14091 } 14092 14093 this.mode_ = 'paintOn'; 14094 this.startPts_ = packet.pts; // Append special characters to caption text 14095 } else if (this.isSpecialCharacter(char0, char1)) { 14096 // Bitmask char0 so that we can apply character transformations 14097 // regardless of field and data channel. 14098 // Then byte-shift to the left and OR with char1 so we can pass the 14099 // entire character code to `getCharFromCode`. 14100 char0 = (char0 & 0x03) << 8; 14101 text = getCharFromCode(char0 | char1); 14102 this[this.mode_](packet.pts, text); 14103 this.column_++; // Append extended characters to caption text 14104 } else if (this.isExtCharacter(char0, char1)) { 14105 // Extended characters always follow their "non-extended" equivalents. 14106 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 14107 // decoders are supposed to drop the "è", while compliant decoders 14108 // backspace the "e" and insert "è". 14109 // Delete the previous character 14110 if (this.mode_ === 'popOn') { 14111 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 14112 } else { 14113 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 14114 } // Bitmask char0 so that we can apply character transformations 14115 // regardless of field and data channel. 14116 // Then byte-shift to the left and OR with char1 so we can pass the 14117 // entire character code to `getCharFromCode`. 14118 14119 14120 char0 = (char0 & 0x03) << 8; 14121 text = getCharFromCode(char0 | char1); 14122 this[this.mode_](packet.pts, text); 14123 this.column_++; // Process mid-row codes 14124 } else if (this.isMidRowCode(char0, char1)) { 14125 // Attributes are not additive, so clear all formatting 14126 this.clearFormatting(packet.pts); // According to the standard, mid-row codes 14127 // should be replaced with spaces, so add one now 14128 14129 this[this.mode_](packet.pts, ' '); 14130 this.column_++; 14131 14132 if ((char1 & 0xe) === 0xe) { 14133 this.addFormatting(packet.pts, ['i']); 14134 } 14135 14136 if ((char1 & 0x1) === 0x1) { 14137 this.addFormatting(packet.pts, ['u']); 14138 } // Detect offset control codes and adjust cursor 14139 14140 } else if (this.isOffsetControlCode(char0, char1)) { 14141 // Cursor position is set by indent PAC (see below) in 4-column 14142 // increments, with an additional offset code of 1-3 to reach any 14143 // of the 32 columns specified by CEA-608. So all we need to do 14144 // here is increment the column cursor by the given offset. 14145 const offset = char1 & 0x03; // For an offest value 1-3, set the offset for that caption 14146 // in the non-displayed array. 14147 14148 this.nonDisplayed_[this.row_].offset = offset; 14149 this.column_ += offset; // Detect PACs (Preamble Address Codes) 14150 } else if (this.isPAC(char0, char1)) { 14151 // There's no logic for PAC -> row mapping, so we have to just 14152 // find the row code in an array and use its index :( 14153 var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode 14154 14155 if (this.mode_ === 'rollUp') { 14156 // This implies that the base row is incorrectly set. 14157 // As per the recommendation in CEA-608(Base Row Implementation), defer to the number 14158 // of roll-up rows set. 14159 if (row - this.rollUpRows_ + 1 < 0) { 14160 row = this.rollUpRows_ - 1; 14161 } 14162 14163 this.setRollUp(packet.pts, row); 14164 } // Ensure the row is between 0 and 14, otherwise use the most 14165 // recent or default row. 14166 14167 14168 if (row !== this.row_ && row >= 0 && row <= 14) { 14169 // formatting is only persistent for current row 14170 this.clearFormatting(packet.pts); 14171 this.row_ = row; 14172 } // All PACs can apply underline, so detect and apply 14173 // (All odd-numbered second bytes set underline) 14174 14175 14176 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 14177 this.addFormatting(packet.pts, ['u']); 14178 } 14179 14180 if ((data & 0x10) === 0x10) { 14181 // We've got an indent level code. Each successive even number 14182 // increments the column cursor by 4, so we can get the desired 14183 // column position by bit-shifting to the right (to get n/2) 14184 // and multiplying by 4. 14185 const indentations = (data & 0xe) >> 1; 14186 this.column_ = indentations * 4; // add to the number of indentations for positioning 14187 14188 this.nonDisplayed_[this.row_].indent += indentations; 14189 } 14190 14191 if (this.isColorPAC(char1)) { 14192 // it's a color code, though we only support white, which 14193 // can be either normal or italicized. white italics can be 14194 // either 0x4e or 0x6e depending on the row, so we just 14195 // bitwise-and with 0xe to see if italics should be turned on 14196 if ((char1 & 0xe) === 0xe) { 14197 this.addFormatting(packet.pts, ['i']); 14198 } 14199 } // We have a normal character in char0, and possibly one in char1 14200 14201 } else if (this.isNormalChar(char0)) { 14202 if (char1 === 0x00) { 14203 char1 = null; 14204 } 14205 14206 text = getCharFromCode(char0); 14207 text += getCharFromCode(char1); 14208 this[this.mode_](packet.pts, text); 14209 this.column_ += text.length; 14210 } // finish data processing 14211 14212 }; 14213 }; 14214 14215 Cea608Stream.prototype = new Stream$7(); // Trigger a cue point that captures the current state of the 14216 // display buffer 14217 14218 Cea608Stream.prototype.flushDisplayed = function (pts) { 14219 const logWarning = index => { 14220 this.trigger('log', { 14221 level: 'warn', 14222 message: 'Skipping a malformed 608 caption at index ' + index + '.' 14223 }); 14224 }; 14225 14226 const content = []; 14227 this.displayed_.forEach((row, i) => { 14228 if (row && row.text && row.text.length) { 14229 try { 14230 // remove spaces from the start and end of the string 14231 row.text = row.text.trim(); 14232 } catch (e) { 14233 // Ordinarily, this shouldn't happen. However, caption 14234 // parsing errors should not throw exceptions and 14235 // break playback. 14236 logWarning(i); 14237 } // See the below link for more details on the following fields: 14238 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 14239 14240 14241 if (row.text.length) { 14242 content.push({ 14243 // The text to be displayed in the caption from this specific row, with whitespace removed. 14244 text: row.text, 14245 // Value between 1 and 15 representing the PAC row used to calculate line height. 14246 line: i + 1, 14247 // A number representing the indent position by percentage (CEA-608 PAC indent code). 14248 // The value will be a number between 10 and 80. Offset is used to add an aditional 14249 // value to the position if necessary. 14250 position: 10 + Math.min(70, row.indent * 10) + row.offset * 2.5 14251 }); 14252 } 14253 } else if (row === undefined || row === null) { 14254 logWarning(i); 14255 } 14256 }); 14257 14258 if (content.length) { 14259 this.trigger('data', { 14260 startPts: this.startPts_, 14261 endPts: pts, 14262 content, 14263 stream: this.name_ 14264 }); 14265 } 14266 }; 14267 /** 14268 * Zero out the data, used for startup and on seek 14269 */ 14270 14271 14272 Cea608Stream.prototype.reset = function () { 14273 this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will 14274 // actually display captions. If a caption is shifted to a row 14275 // with a lower index than this, it is cleared from the display 14276 // buffer 14277 14278 this.topRow_ = 0; 14279 this.startPts_ = 0; 14280 this.displayed_ = createDisplayBuffer(); 14281 this.nonDisplayed_ = createDisplayBuffer(); 14282 this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing 14283 14284 this.column_ = 0; 14285 this.row_ = BOTTOM_ROW; 14286 this.rollUpRows_ = 2; // This variable holds currently-applied formatting 14287 14288 this.formatting_ = []; 14289 }; 14290 /** 14291 * Sets up control code and related constants for this instance 14292 */ 14293 14294 14295 Cea608Stream.prototype.setConstants = function () { 14296 // The following attributes have these uses: 14297 // ext_ : char0 for mid-row codes, and the base for extended 14298 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 14299 // extended codes) 14300 // control_: char0 for control codes, except byte-shifted to the 14301 // left so that we can do this.control_ | CONTROL_CODE 14302 // offset_: char0 for tab offset codes 14303 // 14304 // It's also worth noting that control codes, and _only_ control codes, 14305 // differ between field 1 and field2. Field 2 control codes are always 14306 // their field 1 value plus 1. That's why there's the "| field" on the 14307 // control value. 14308 if (this.dataChannel_ === 0) { 14309 this.BASE_ = 0x10; 14310 this.EXT_ = 0x11; 14311 this.CONTROL_ = (0x14 | this.field_) << 8; 14312 this.OFFSET_ = 0x17; 14313 } else if (this.dataChannel_ === 1) { 14314 this.BASE_ = 0x18; 14315 this.EXT_ = 0x19; 14316 this.CONTROL_ = (0x1c | this.field_) << 8; 14317 this.OFFSET_ = 0x1f; 14318 } // Constants for the LSByte command codes recognized by Cea608Stream. This 14319 // list is not exhaustive. For a more comprehensive listing and semantics see 14320 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 14321 // Padding 14322 14323 14324 this.PADDING_ = 0x0000; // Pop-on Mode 14325 14326 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 14327 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode 14328 14329 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 14330 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 14331 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 14332 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode 14333 14334 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure 14335 14336 this.BACKSPACE_ = this.CONTROL_ | 0x21; 14337 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 14338 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 14339 }; 14340 /** 14341 * Detects if the 2-byte packet data is a special character 14342 * 14343 * Special characters have a second byte in the range 0x30 to 0x3f, 14344 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 14345 * data channel 2). 14346 * 14347 * @param {Integer} char0 The first byte 14348 * @param {Integer} char1 The second byte 14349 * @return {Boolean} Whether the 2 bytes are an special character 14350 */ 14351 14352 14353 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 14354 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 14355 }; 14356 /** 14357 * Detects if the 2-byte packet data is an extended character 14358 * 14359 * Extended characters have a second byte in the range 0x20 to 0x3f, 14360 * with the first byte being 0x12 or 0x13 (for data channel 1) or 14361 * 0x1a or 0x1b (for data channel 2). 14362 * 14363 * @param {Integer} char0 The first byte 14364 * @param {Integer} char1 The second byte 14365 * @return {Boolean} Whether the 2 bytes are an extended character 14366 */ 14367 14368 14369 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 14370 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 14371 }; 14372 /** 14373 * Detects if the 2-byte packet is a mid-row code 14374 * 14375 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 14376 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 14377 * channel 2). 14378 * 14379 * @param {Integer} char0 The first byte 14380 * @param {Integer} char1 The second byte 14381 * @return {Boolean} Whether the 2 bytes are a mid-row code 14382 */ 14383 14384 14385 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 14386 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 14387 }; 14388 /** 14389 * Detects if the 2-byte packet is an offset control code 14390 * 14391 * Offset control codes have a second byte in the range 0x21 to 0x23, 14392 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 14393 * data channel 2). 14394 * 14395 * @param {Integer} char0 The first byte 14396 * @param {Integer} char1 The second byte 14397 * @return {Boolean} Whether the 2 bytes are an offset control code 14398 */ 14399 14400 14401 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 14402 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 14403 }; 14404 /** 14405 * Detects if the 2-byte packet is a Preamble Address Code 14406 * 14407 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 14408 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 14409 * range 0x40 to 0x7f. 14410 * 14411 * @param {Integer} char0 The first byte 14412 * @param {Integer} char1 The second byte 14413 * @return {Boolean} Whether the 2 bytes are a PAC 14414 */ 14415 14416 14417 Cea608Stream.prototype.isPAC = function (char0, char1) { 14418 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 14419 }; 14420 /** 14421 * Detects if a packet's second byte is in the range of a PAC color code 14422 * 14423 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 14424 * 0x60 to 0x6f. 14425 * 14426 * @param {Integer} char1 The second byte 14427 * @return {Boolean} Whether the byte is a color PAC 14428 */ 14429 14430 14431 Cea608Stream.prototype.isColorPAC = function (char1) { 14432 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 14433 }; 14434 /** 14435 * Detects if a single byte is in the range of a normal character 14436 * 14437 * Normal text bytes are in the range 0x20 to 0x7f. 14438 * 14439 * @param {Integer} char The byte 14440 * @return {Boolean} Whether the byte is a normal character 14441 */ 14442 14443 14444 Cea608Stream.prototype.isNormalChar = function (char) { 14445 return char >= 0x20 && char <= 0x7f; 14446 }; 14447 /** 14448 * Configures roll-up 14449 * 14450 * @param {Integer} pts Current PTS 14451 * @param {Integer} newBaseRow Used by PACs to slide the current window to 14452 * a new position 14453 */ 14454 14455 14456 Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) { 14457 // Reset the base row to the bottom row when switching modes 14458 if (this.mode_ !== 'rollUp') { 14459 this.row_ = BOTTOM_ROW; 14460 this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up 14461 14462 this.flushDisplayed(pts); 14463 this.nonDisplayed_ = createDisplayBuffer(); 14464 this.displayed_ = createDisplayBuffer(); 14465 } 14466 14467 if (newBaseRow !== undefined && newBaseRow !== this.row_) { 14468 // move currently displayed captions (up or down) to the new base row 14469 for (var i = 0; i < this.rollUpRows_; i++) { 14470 this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; 14471 this.displayed_[this.row_ - i] = { 14472 text: '', 14473 indent: 0, 14474 offset: 0 14475 }; 14476 } 14477 } 14478 14479 if (newBaseRow === undefined) { 14480 newBaseRow = this.row_; 14481 } 14482 14483 this.topRow_ = newBaseRow - this.rollUpRows_ + 1; 14484 }; // Adds the opening HTML tag for the passed character to the caption text, 14485 // and keeps track of it for later closing 14486 14487 14488 Cea608Stream.prototype.addFormatting = function (pts, format) { 14489 this.formatting_ = this.formatting_.concat(format); 14490 var text = format.reduce(function (text, format) { 14491 return text + '<' + format + '>'; 14492 }, ''); 14493 this[this.mode_](pts, text); 14494 }; // Adds HTML closing tags for current formatting to caption text and 14495 // clears remembered formatting 14496 14497 14498 Cea608Stream.prototype.clearFormatting = function (pts) { 14499 if (!this.formatting_.length) { 14500 return; 14501 } 14502 14503 var text = this.formatting_.reverse().reduce(function (text, format) { 14504 return text + '</' + format + '>'; 14505 }, ''); 14506 this.formatting_ = []; 14507 this[this.mode_](pts, text); 14508 }; // Mode Implementations 14509 14510 14511 Cea608Stream.prototype.popOn = function (pts, text) { 14512 var baseRow = this.nonDisplayed_[this.row_].text; // buffer characters 14513 14514 baseRow += text; 14515 this.nonDisplayed_[this.row_].text = baseRow; 14516 }; 14517 14518 Cea608Stream.prototype.rollUp = function (pts, text) { 14519 var baseRow = this.displayed_[this.row_].text; 14520 baseRow += text; 14521 this.displayed_[this.row_].text = baseRow; 14522 }; 14523 14524 Cea608Stream.prototype.shiftRowsUp_ = function () { 14525 var i; // clear out inactive rows 14526 14527 for (i = 0; i < this.topRow_; i++) { 14528 this.displayed_[i] = { 14529 text: '', 14530 indent: 0, 14531 offset: 0 14532 }; 14533 } 14534 14535 for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { 14536 this.displayed_[i] = { 14537 text: '', 14538 indent: 0, 14539 offset: 0 14540 }; 14541 } // shift displayed rows up 14542 14543 14544 for (i = this.topRow_; i < this.row_; i++) { 14545 this.displayed_[i] = this.displayed_[i + 1]; 14546 } // clear out the bottom row 14547 14548 14549 this.displayed_[this.row_] = { 14550 text: '', 14551 indent: 0, 14552 offset: 0 14553 }; 14554 }; 14555 14556 Cea608Stream.prototype.paintOn = function (pts, text) { 14557 var baseRow = this.displayed_[this.row_].text; 14558 baseRow += text; 14559 this.displayed_[this.row_].text = baseRow; 14560 }; // exports 14561 14562 14563 var captionStream = { 14564 CaptionStream: CaptionStream$2, 14565 Cea608Stream: Cea608Stream, 14566 Cea708Stream: Cea708Stream 14567 }; 14568 /** 14569 * mux.js 14570 * 14571 * Copyright (c) Brightcove 14572 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14573 */ 14574 14575 var streamTypes = { 14576 H264_STREAM_TYPE: 0x1B, 14577 ADTS_STREAM_TYPE: 0x0F, 14578 METADATA_STREAM_TYPE: 0x15 14579 }; 14580 /** 14581 * mux.js 14582 * 14583 * Copyright (c) Brightcove 14584 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14585 * 14586 * Accepts program elementary stream (PES) data events and corrects 14587 * decode and presentation time stamps to account for a rollover 14588 * of the 33 bit value. 14589 */ 14590 14591 var Stream$6 = stream; 14592 var MAX_TS = 8589934592; 14593 var RO_THRESH = 4294967296; 14594 var TYPE_SHARED = 'shared'; 14595 14596 var handleRollover$1 = function (value, reference) { 14597 var direction = 1; 14598 14599 if (value > reference) { 14600 // If the current timestamp value is greater than our reference timestamp and we detect a 14601 // timestamp rollover, this means the roll over is happening in the opposite direction. 14602 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 14603 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 14604 // rollover point. In loading this segment, the timestamp values will be very large, 14605 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 14606 // the time stamp to be `value - 2^33`. 14607 direction = -1; 14608 } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 14609 // cause an incorrect adjustment. 14610 14611 14612 while (Math.abs(reference - value) > RO_THRESH) { 14613 value += direction * MAX_TS; 14614 } 14615 14616 return value; 14617 }; 14618 14619 var TimestampRolloverStream$1 = function (type) { 14620 var lastDTS, referenceDTS; 14621 TimestampRolloverStream$1.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed 14622 // video and audio. We could use `undefined` here, but having a string 14623 // makes debugging a little clearer. 14624 14625 this.type_ = type || TYPE_SHARED; 14626 14627 this.push = function (data) { 14628 /** 14629 * Rollover stream expects data from elementary stream. 14630 * Elementary stream can push forward 2 types of data 14631 * - Parsed Video/Audio/Timed-metadata PES (packetized elementary stream) packets 14632 * - Tracks metadata from PMT (Program Map Table) 14633 * Rollover stream expects pts/dts info to be available, since it stores lastDTS 14634 * We should ignore non-PES packets since they may override lastDTS to undefined. 14635 * lastDTS is important to signal the next segments 14636 * about rollover from the previous segments. 14637 */ 14638 if (data.type === 'metadata') { 14639 this.trigger('data', data); 14640 return; 14641 } // Any "shared" rollover streams will accept _all_ data. Otherwise, 14642 // streams will only accept data that matches their type. 14643 14644 14645 if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { 14646 return; 14647 } 14648 14649 if (referenceDTS === undefined) { 14650 referenceDTS = data.dts; 14651 } 14652 14653 data.dts = handleRollover$1(data.dts, referenceDTS); 14654 data.pts = handleRollover$1(data.pts, referenceDTS); 14655 lastDTS = data.dts; 14656 this.trigger('data', data); 14657 }; 14658 14659 this.flush = function () { 14660 referenceDTS = lastDTS; 14661 this.trigger('done'); 14662 }; 14663 14664 this.endTimeline = function () { 14665 this.flush(); 14666 this.trigger('endedtimeline'); 14667 }; 14668 14669 this.discontinuity = function () { 14670 referenceDTS = void 0; 14671 lastDTS = void 0; 14672 }; 14673 14674 this.reset = function () { 14675 this.discontinuity(); 14676 this.trigger('reset'); 14677 }; 14678 }; 14679 14680 TimestampRolloverStream$1.prototype = new Stream$6(); 14681 var timestampRolloverStream = { 14682 TimestampRolloverStream: TimestampRolloverStream$1, 14683 handleRollover: handleRollover$1 14684 }; // Once IE11 support is dropped, this function should be removed. 14685 14686 var typedArrayIndexOf$1 = (typedArray, element, fromIndex) => { 14687 if (!typedArray) { 14688 return -1; 14689 } 14690 14691 var currentIndex = fromIndex; 14692 14693 for (; currentIndex < typedArray.length; currentIndex++) { 14694 if (typedArray[currentIndex] === element) { 14695 return currentIndex; 14696 } 14697 } 14698 14699 return -1; 14700 }; 14701 14702 var typedArray = { 14703 typedArrayIndexOf: typedArrayIndexOf$1 14704 }; 14705 /** 14706 * mux.js 14707 * 14708 * Copyright (c) Brightcove 14709 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14710 * 14711 * Tools for parsing ID3 frame data 14712 * @see http://id3.org/id3v2.3.0 14713 */ 14714 14715 var typedArrayIndexOf = typedArray.typedArrayIndexOf, 14716 // Frames that allow different types of text encoding contain a text 14717 // encoding description byte [ID3v2.4.0 section 4.] 14718 textEncodingDescriptionByte = { 14719 Iso88591: 0x00, 14720 // ISO-8859-1, terminated with \0. 14721 Utf16: 0x01, 14722 // UTF-16 encoded Unicode BOM, terminated with \0\0 14723 Utf16be: 0x02, 14724 // UTF-16BE encoded Unicode, without BOM, terminated with \0\0 14725 Utf8: 0x03 // UTF-8 encoded Unicode, terminated with \0 14726 14727 }, 14728 // return a percent-encoded representation of the specified byte range 14729 // @see http://en.wikipedia.org/wiki/Percent-encoding
vendor: 5,570 bytes, lines 14729-14879
14729 14730 percentEncode$1 = function (bytes, start, end) { 14731 var i, 14732 result = ''; 14733 14734 for (i = start; i < end; i++) { 14735 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 14736 } 14737 14738 return result; 14739 }, 14740 // return the string representation of the specified byte range, 14741 // interpreted as UTf-8. 14742 parseUtf8 = function (bytes, start, end) { 14743 return decodeURIComponent(percentEncode$1(bytes, start, end)); 14744 }, 14745 // return the string representation of the specified byte range, 14746 // interpreted as ISO-8859-1. 14747 parseIso88591$1 = function (bytes, start, end) { 14748 return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line 14749 }, 14750 parseSyncSafeInteger$1 = function (data) { 14751 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 14752 }, 14753 frameParsers = { 14754 'APIC': function (frame) { 14755 var i = 1, 14756 mimeTypeEndIndex, 14757 descriptionEndIndex, 14758 LINK_MIME_TYPE = '-->'; 14759 14760 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14761 // ignore frames with unrecognized character encodings 14762 return; 14763 } // parsing fields [ID3v2.4.0 section 4.14.] 14764 14765 14766 mimeTypeEndIndex = typedArrayIndexOf(frame.data, 0, i); 14767 14768 if (mimeTypeEndIndex < 0) { 14769 // malformed frame 14770 return; 14771 } // parsing Mime type field (terminated with \0) 14772 14773 14774 frame.mimeType = parseIso88591$1(frame.data, i, mimeTypeEndIndex); 14775 i = mimeTypeEndIndex + 1; // parsing 1-byte Picture Type field 14776 14777 frame.pictureType = frame.data[i]; 14778 i++; 14779 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, i); 14780 14781 if (descriptionEndIndex < 0) { 14782 // malformed frame 14783 return; 14784 } // parsing Description field (terminated with \0) 14785 14786 14787 frame.description = parseUtf8(frame.data, i, descriptionEndIndex); 14788 i = descriptionEndIndex + 1; 14789 14790 if (frame.mimeType === LINK_MIME_TYPE) { 14791 // parsing Picture Data field as URL (always represented as ISO-8859-1 [ID3v2.4.0 section 4.]) 14792 frame.url = parseIso88591$1(frame.data, i, frame.data.length); 14793 } else { 14794 // parsing Picture Data field as binary data 14795 frame.pictureData = frame.data.subarray(i, frame.data.length); 14796 } 14797 }, 14798 'T*': function (frame) { 14799 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14800 // ignore frames with unrecognized character encodings 14801 return; 14802 } // parse text field, do not include null terminator in the frame value 14803 // frames that allow different types of encoding contain terminated text [ID3v2.4.0 section 4.] 14804 14805 14806 frame.value = parseUtf8(frame.data, 1, frame.data.length).replace(/\0*$/, ''); // text information frames supports multiple strings, stored as a terminator separated list [ID3v2.4.0 section 4.2.] 14807 14808 frame.values = frame.value.split('\0'); 14809 }, 14810 'TXXX': function (frame) { 14811 var descriptionEndIndex; 14812 14813 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14814 // ignore frames with unrecognized character encodings 14815 return; 14816 } 14817 14818 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 14819 14820 if (descriptionEndIndex === -1) { 14821 return; 14822 } // parse the text fields 14823 14824 14825 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // do not include the null terminator in the tag value 14826 // frames that allow different types of encoding contain terminated text 14827 // [ID3v2.4.0 section 4.] 14828 14829 frame.value = parseUtf8(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0*$/, ''); 14830 frame.data = frame.value; 14831 }, 14832 'W*': function (frame) { 14833 // parse URL field; URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 14834 // if the value is followed by a string termination all the following information should be ignored [ID3v2.4.0 section 4.3] 14835 frame.url = parseIso88591$1(frame.data, 0, frame.data.length).replace(/\0.*$/, ''); 14836 }, 14837 'WXXX': function (frame) { 14838 var descriptionEndIndex; 14839 14840 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14841 // ignore frames with unrecognized character encodings 14842 return; 14843 } 14844 14845 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 14846 14847 if (descriptionEndIndex === -1) { 14848 return; 14849 } // parse the description and URL fields 14850 14851 14852 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 14853 // if the value is followed by a string termination all the following information 14854 // should be ignored [ID3v2.4.0 section 4.3] 14855 14856 frame.url = parseIso88591$1(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0.*$/, ''); 14857 }, 14858 'PRIV': function (frame) { 14859 var i; 14860 14861 for (i = 0; i < frame.data.length; i++) { 14862 if (frame.data[i] === 0) { 14863 // parse the description and URL fields 14864 frame.owner = parseIso88591$1(frame.data, 0, i); 14865 break; 14866 } 14867 } 14868 14869 frame.privateData = frame.data.subarray(i + 1); 14870 frame.data = frame.privateData; 14871 } 14872 }; 14873 14874 var parseId3Frames$1 = function (data) { 14875 var frameSize, 14876 frameHeader, 14877 frameStart = 10, 14878 tagSize = 0, 14879 frames = []; // If we don't have enough data for a header, 10 bytes,
vendor: 141,022 bytes, lines 14879-18979
14879 14880 // or 'ID3' in the first 3 bytes this is not a valid ID3 tag. 14881 14882 if (data.length < 10 || data[0] !== 'I'.charCodeAt(0) || data[1] !== 'D'.charCodeAt(0) || data[2] !== '3'.charCodeAt(0)) { 14883 return; 14884 } // the frame size is transmitted as a 28-bit integer in the 14885 // last four bytes of the ID3 header. 14886 // The most significant bit of each byte is dropped and the 14887 // results concatenated to recover the actual value. 14888 14889 14890 tagSize = parseSyncSafeInteger$1(data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 14891 // convenient for our comparisons to include it 14892 14893 tagSize += 10; // check bit 6 of byte 5 for the extended header flag. 14894 14895 var hasExtendedHeader = data[5] & 0x40; 14896 14897 if (hasExtendedHeader) { 14898 // advance the frame start past the extended header 14899 frameStart += 4; // header size field 14900 14901 frameStart += parseSyncSafeInteger$1(data.subarray(10, 14)); 14902 tagSize -= parseSyncSafeInteger$1(data.subarray(16, 20)); // clip any padding off the end 14903 } // parse one or more ID3 frames 14904 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 14905 14906 14907 do { 14908 // determine the number of bytes in this frame 14909 frameSize = parseSyncSafeInteger$1(data.subarray(frameStart + 4, frameStart + 8)); 14910 14911 if (frameSize < 1) { 14912 break; 14913 } 14914 14915 frameHeader = String.fromCharCode(data[frameStart], data[frameStart + 1], data[frameStart + 2], data[frameStart + 3]); 14916 var frame = { 14917 id: frameHeader, 14918 data: data.subarray(frameStart + 10, frameStart + frameSize + 10) 14919 }; 14920 frame.key = frame.id; // parse frame values 14921 14922 if (frameParsers[frame.id]) { 14923 // use frame specific parser 14924 frameParsers[frame.id](frame); 14925 } else if (frame.id[0] === 'T') { 14926 // use text frame generic parser 14927 frameParsers['T*'](frame); 14928 } else if (frame.id[0] === 'W') { 14929 // use URL link frame generic parser 14930 frameParsers['W*'](frame); 14931 } 14932 14933 frames.push(frame); 14934 frameStart += 10; // advance past the frame header 14935 14936 frameStart += frameSize; // advance past the frame body 14937 } while (frameStart < tagSize); 14938 14939 return frames; 14940 }; 14941 14942 var parseId3 = { 14943 parseId3Frames: parseId3Frames$1, 14944 parseSyncSafeInteger: parseSyncSafeInteger$1, 14945 frameParsers: frameParsers 14946 }; 14947 /** 14948 * mux.js 14949 * 14950 * Copyright (c) Brightcove 14951 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14952 * 14953 * Accepts program elementary stream (PES) data events and parses out 14954 * ID3 metadata from them, if present. 14955 * @see http://id3.org/id3v2.3.0 14956 */ 14957 14958 var Stream$5 = stream, 14959 StreamTypes$3 = streamTypes, 14960 id3 = parseId3, 14961 MetadataStream; 14962 14963 MetadataStream = function (options) { 14964 var settings = { 14965 // the bytes of the program-level descriptor field in MP2T 14966 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 14967 // program element descriptors" 14968 descriptor: options && options.descriptor 14969 }, 14970 // the total size in bytes of the ID3 tag being parsed 14971 tagSize = 0, 14972 // tag data that is not complete enough to be parsed 14973 buffer = [], 14974 // the total number of bytes currently in the buffer 14975 bufferSize = 0, 14976 i; 14977 MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type 14978 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 14979 14980 this.dispatchType = StreamTypes$3.METADATA_STREAM_TYPE.toString(16); 14981 14982 if (settings.descriptor) { 14983 for (i = 0; i < settings.descriptor.length; i++) { 14984 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 14985 } 14986 } 14987 14988 this.push = function (chunk) { 14989 var tag, frameStart, frameSize, frame, i, frameHeader; 14990 14991 if (chunk.type !== 'timed-metadata') { 14992 return; 14993 } // if data_alignment_indicator is set in the PES header, 14994 // we must have the start of a new ID3 tag. Assume anything 14995 // remaining in the buffer was malformed and throw it out 14996 14997 14998 if (chunk.dataAlignmentIndicator) { 14999 bufferSize = 0; 15000 buffer.length = 0; 15001 } // ignore events that don't look like ID3 data 15002 15003 15004 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))) { 15005 this.trigger('log', { 15006 level: 'warn', 15007 message: 'Skipping unrecognized metadata packet' 15008 }); 15009 return; 15010 } // add this chunk to the data we've collected so far 15011 15012 15013 buffer.push(chunk); 15014 bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header 15015 15016 if (buffer.length === 1) { 15017 // the frame size is transmitted as a 28-bit integer in the 15018 // last four bytes of the ID3 header. 15019 // The most significant bit of each byte is dropped and the 15020 // results concatenated to recover the actual value. 15021 tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 15022 // convenient for our comparisons to include it 15023 15024 tagSize += 10; 15025 } // if the entire frame has not arrived, wait for more data 15026 15027 15028 if (bufferSize < tagSize) { 15029 return; 15030 } // collect the entire frame so it can be parsed 15031 15032 15033 tag = { 15034 data: new Uint8Array(tagSize), 15035 frames: [], 15036 pts: buffer[0].pts, 15037 dts: buffer[0].dts 15038 }; 15039 15040 for (i = 0; i < tagSize;) { 15041 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 15042 i += buffer[0].data.byteLength; 15043 bufferSize -= buffer[0].data.byteLength; 15044 buffer.shift(); 15045 } // find the start of the first frame and the end of the tag 15046 15047 15048 frameStart = 10; 15049 15050 if (tag.data[5] & 0x40) { 15051 // advance the frame start past the extended header 15052 frameStart += 4; // header size field 15053 15054 frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end 15055 15056 tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20)); 15057 } // parse one or more ID3 frames 15058 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 15059 15060 15061 do { 15062 // determine the number of bytes in this frame 15063 frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 15064 15065 if (frameSize < 1) { 15066 this.trigger('log', { 15067 level: 'warn', 15068 message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.' 15069 }); // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames 15070 // to be sent along. 15071 15072 break; 15073 } 15074 15075 frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]); 15076 frame = { 15077 id: frameHeader, 15078 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) 15079 }; 15080 frame.key = frame.id; // parse frame values 15081 15082 if (id3.frameParsers[frame.id]) { 15083 // use frame specific parser 15084 id3.frameParsers[frame.id](frame); 15085 } else if (frame.id[0] === 'T') { 15086 // use text frame generic parser 15087 id3.frameParsers['T*'](frame); 15088 } else if (frame.id[0] === 'W') { 15089 // use URL link frame generic parser 15090 id3.frameParsers['W*'](frame); 15091 } // handle the special PRIV frame used to indicate the start 15092 // time for raw AAC data 15093 15094 15095 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 15096 var d = frame.data, 15097 size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 15098 size *= 4; 15099 size += d[7] & 0x03; 15100 frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based 15101 // on the value of this frame 15102 // we couldn't have known the appropriate pts and dts before 15103 // parsing this ID3 tag so set those values now 15104 15105 if (tag.pts === undefined && tag.dts === undefined) { 15106 tag.pts = frame.timeStamp; 15107 tag.dts = frame.timeStamp; 15108 } 15109 15110 this.trigger('timestamp', frame); 15111 } 15112 15113 tag.frames.push(frame); 15114 frameStart += 10; // advance past the frame header 15115 15116 frameStart += frameSize; // advance past the frame body 15117 } while (frameStart < tagSize); 15118 15119 this.trigger('data', tag); 15120 }; 15121 }; 15122 15123 MetadataStream.prototype = new Stream$5(); 15124 var metadataStream = MetadataStream; 15125 /** 15126 * mux.js 15127 * 15128 * Copyright (c) Brightcove 15129 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15130 * 15131 * A stream-based mp2t to mp4 converter. This utility can be used to 15132 * deliver mp4s to a SourceBuffer on platforms that support native 15133 * Media Source Extensions. 15134 */ 15135 15136 var Stream$4 = stream, 15137 CaptionStream$1 = captionStream, 15138 StreamTypes$2 = streamTypes, 15139 TimestampRolloverStream = timestampRolloverStream.TimestampRolloverStream; // object types 15140 15141 var TransportPacketStream, TransportParseStream, ElementaryStream; // constants 15142 15143 var MP2T_PACKET_LENGTH$1 = 188, 15144 // bytes 15145 SYNC_BYTE$1 = 0x47; 15146 /** 15147 * Splits an incoming stream of binary data into MPEG-2 Transport 15148 * Stream packets. 15149 */ 15150 15151 TransportPacketStream = function () { 15152 var buffer = new Uint8Array(MP2T_PACKET_LENGTH$1), 15153 bytesInBuffer = 0; 15154 TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream. 15155 15156 /** 15157 * Split a stream of data into M2TS packets 15158 **/ 15159 15160 this.push = function (bytes) { 15161 var startIndex = 0, 15162 endIndex = MP2T_PACKET_LENGTH$1, 15163 everything; // If there are bytes remaining from the last segment, prepend them to the 15164 // bytes that were pushed in 15165 15166 if (bytesInBuffer) { 15167 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 15168 everything.set(buffer.subarray(0, bytesInBuffer)); 15169 everything.set(bytes, bytesInBuffer); 15170 bytesInBuffer = 0; 15171 } else { 15172 everything = bytes; 15173 } // While we have enough data for a packet 15174 15175 15176 while (endIndex < everything.byteLength) { 15177 // Look for a pair of start and end sync bytes in the data.. 15178 if (everything[startIndex] === SYNC_BYTE$1 && everything[endIndex] === SYNC_BYTE$1) { 15179 // We found a packet so emit it and jump one whole packet forward in 15180 // the stream 15181 this.trigger('data', everything.subarray(startIndex, endIndex)); 15182 startIndex += MP2T_PACKET_LENGTH$1; 15183 endIndex += MP2T_PACKET_LENGTH$1; 15184 continue; 15185 } // If we get here, we have somehow become de-synchronized and we need to step 15186 // forward one byte at a time until we find a pair of sync bytes that denote 15187 // a packet 15188 15189 15190 startIndex++; 15191 endIndex++; 15192 } // If there was some data left over at the end of the segment that couldn't 15193 // possibly be a whole packet, keep it because it might be the start of a packet 15194 // that continues in the next segment 15195 15196 15197 if (startIndex < everything.byteLength) { 15198 buffer.set(everything.subarray(startIndex), 0); 15199 bytesInBuffer = everything.byteLength - startIndex; 15200 } 15201 }; 15202 /** 15203 * Passes identified M2TS packets to the TransportParseStream to be parsed 15204 **/ 15205 15206 15207 this.flush = function () { 15208 // If the buffer contains a whole packet when we are being flushed, emit it 15209 // and empty the buffer. Otherwise hold onto the data because it may be 15210 // important for decoding the next segment 15211 if (bytesInBuffer === MP2T_PACKET_LENGTH$1 && buffer[0] === SYNC_BYTE$1) { 15212 this.trigger('data', buffer); 15213 bytesInBuffer = 0; 15214 } 15215 15216 this.trigger('done'); 15217 }; 15218 15219 this.endTimeline = function () { 15220 this.flush(); 15221 this.trigger('endedtimeline'); 15222 }; 15223 15224 this.reset = function () { 15225 bytesInBuffer = 0; 15226 this.trigger('reset'); 15227 }; 15228 }; 15229 15230 TransportPacketStream.prototype = new Stream$4(); 15231 /** 15232 * Accepts an MP2T TransportPacketStream and emits data events with parsed 15233 * forms of the individual transport stream packets. 15234 */ 15235 15236 TransportParseStream = function () { 15237 var parsePsi, parsePat, parsePmt, self; 15238 TransportParseStream.prototype.init.call(this); 15239 self = this; 15240 this.packetsWaitingForPmt = []; 15241 this.programMapTable = undefined; 15242 15243 parsePsi = function (payload, psi) { 15244 var offset = 0; // PSI packets may be split into multiple sections and those 15245 // sections may be split into multiple packets. If a PSI 15246 // section starts in this packet, the payload_unit_start_indicator 15247 // will be true and the first byte of the payload will indicate 15248 // the offset from the current position to the start of the 15249 // section. 15250 15251 if (psi.payloadUnitStartIndicator) { 15252 offset += payload[offset] + 1; 15253 } 15254 15255 if (psi.type === 'pat') { 15256 parsePat(payload.subarray(offset), psi); 15257 } else { 15258 parsePmt(payload.subarray(offset), psi); 15259 } 15260 }; 15261 15262 parsePat = function (payload, pat) { 15263 pat.section_number = payload[7]; // eslint-disable-line camelcase 15264 15265 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 15266 // skip the PSI header and parse the first PMT entry 15267 15268 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; 15269 pat.pmtPid = self.pmtPid; 15270 }; 15271 /** 15272 * Parse out the relevant fields of a Program Map Table (PMT). 15273 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 15274 * packet. The first byte in this array should be the table_id 15275 * field. 15276 * @param pmt {object} the object that should be decorated with 15277 * fields parsed from the PMT. 15278 */ 15279 15280 15281 parsePmt = function (payload, pmt) { 15282 var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually 15283 // take effect. We don't believe this should ever be the case 15284 // for HLS but we'll ignore "forward" PMT declarations if we see 15285 // them. Future PMT declarations have the current_next_indicator 15286 // set to zero. 15287 15288 if (!(payload[5] & 0x01)) { 15289 return; 15290 } // overwrite any existing program map table 15291 15292 15293 self.programMapTable = { 15294 video: null, 15295 audio: null, 15296 'timed-metadata': {} 15297 }; // the mapping table ends at the end of the current section 15298 15299 sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; 15300 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 15301 // long the program info descriptors are 15302 15303 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table 15304 15305 offset = 12 + programInfoLength; 15306 15307 while (offset < tableEnd) { 15308 var streamType = payload[offset]; 15309 var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types 15310 // TODO: should this be done for metadata too? for now maintain behavior of 15311 // multiple metadata streams 15312 15313 if (streamType === StreamTypes$2.H264_STREAM_TYPE && self.programMapTable.video === null) { 15314 self.programMapTable.video = pid; 15315 } else if (streamType === StreamTypes$2.ADTS_STREAM_TYPE && self.programMapTable.audio === null) { 15316 self.programMapTable.audio = pid; 15317 } else if (streamType === StreamTypes$2.METADATA_STREAM_TYPE) { 15318 // map pid to stream type for metadata streams 15319 self.programMapTable['timed-metadata'][pid] = streamType; 15320 } // move to the next table entry 15321 // skip past the elementary stream descriptors, if present 15322 15323 15324 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; 15325 } // record the map on the packet as well 15326 15327 15328 pmt.programMapTable = self.programMapTable; 15329 }; 15330 /** 15331 * Deliver a new MP2T packet to the next stream in the pipeline. 15332 */ 15333 15334 15335 this.push = function (packet) { 15336 var result = {}, 15337 offset = 4; 15338 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1] 15339 15340 result.pid = packet[1] & 0x1f; 15341 result.pid <<= 8; 15342 result.pid |= packet[2]; // if an adaption field is present, its length is specified by the 15343 // fifth byte of the TS packet header. The adaptation field is 15344 // used to add stuffing to PES packets that don't fill a complete 15345 // TS packet, and to specify some forms of timing and control data 15346 // that we do not currently use. 15347 15348 if ((packet[3] & 0x30) >>> 4 > 0x01) { 15349 offset += packet[offset] + 1; 15350 } // parse the rest of the packet based on the type 15351 15352 15353 if (result.pid === 0) { 15354 result.type = 'pat'; 15355 parsePsi(packet.subarray(offset), result); 15356 this.trigger('data', result); 15357 } else if (result.pid === this.pmtPid) { 15358 result.type = 'pmt'; 15359 parsePsi(packet.subarray(offset), result); 15360 this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now 15361 15362 while (this.packetsWaitingForPmt.length) { 15363 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 15364 } 15365 } else if (this.programMapTable === undefined) { 15366 // When we have not seen a PMT yet, defer further processing of 15367 // PES packets until one has been parsed 15368 this.packetsWaitingForPmt.push([packet, offset, result]); 15369 } else { 15370 this.processPes_(packet, offset, result); 15371 } 15372 }; 15373 15374 this.processPes_ = function (packet, offset, result) { 15375 // set the appropriate stream type 15376 if (result.pid === this.programMapTable.video) { 15377 result.streamType = StreamTypes$2.H264_STREAM_TYPE; 15378 } else if (result.pid === this.programMapTable.audio) { 15379 result.streamType = StreamTypes$2.ADTS_STREAM_TYPE; 15380 } else { 15381 // if not video or audio, it is timed-metadata or unknown 15382 // if unknown, streamType will be undefined 15383 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 15384 } 15385 15386 result.type = 'pes'; 15387 result.data = packet.subarray(offset); 15388 this.trigger('data', result); 15389 }; 15390 }; 15391 15392 TransportParseStream.prototype = new Stream$4(); 15393 TransportParseStream.STREAM_TYPES = { 15394 h264: 0x1b, 15395 adts: 0x0f 15396 }; 15397 /** 15398 * Reconsistutes program elementary stream (PES) packets from parsed 15399 * transport stream packets. That is, if you pipe an 15400 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 15401 * events will be events which capture the bytes for individual PES 15402 * packets plus relevant metadata that has been extracted from the 15403 * container. 15404 */ 15405 15406 ElementaryStream = function () { 15407 var self = this, 15408 segmentHadPmt = false, 15409 // PES packet fragments 15410 video = { 15411 data: [], 15412 size: 0 15413 }, 15414 audio = { 15415 data: [], 15416 size: 0 15417 }, 15418 timedMetadata = { 15419 data: [], 15420 size: 0 15421 }, 15422 programMapTable, 15423 parsePes = function (payload, pes) { 15424 var ptsDtsFlags; 15425 const startPrefix = payload[0] << 16 | payload[1] << 8 | payload[2]; // default to an empty array 15426 15427 pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets 15428 // that are frame data that is continuing from the previous fragment. This 15429 // is to check that the pes data is the start of a new pes payload 15430 15431 if (startPrefix !== 1) { 15432 return; 15433 } // get the packet length, this will be 0 for video 15434 15435 15436 pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe 15437 15438 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value 15439 // and a DTS value. Determine what combination of values is 15440 // available to work with. 15441 15442 ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 15443 // performs all bitwise operations on 32-bit integers but javascript 15444 // supports a much greater range (52-bits) of integer using standard 15445 // mathematical operations. 15446 // We construct a 31-bit value using bitwise operators over the 31 15447 // most significant bits and then multiply by 4 (equal to a left-shift 15448 // of 2) before we add the final 2 least significant bits of the 15449 // timestamp (equal to an OR.) 15450 15451 if (ptsDtsFlags & 0xC0) { 15452 // the PTS and DTS are not written out directly. For information 15453 // on how they are encoded, see 15454 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 15455 pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3; 15456 pes.pts *= 4; // Left shift by 2 15457 15458 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 15459 15460 pes.dts = pes.pts; 15461 15462 if (ptsDtsFlags & 0x40) { 15463 pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3; 15464 pes.dts *= 4; // Left shift by 2 15465 15466 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 15467 } 15468 } // the data section starts immediately after the PES header. 15469 // pes_header_data_length specifies the number of header bytes 15470 // that follow the last byte of the field. 15471 15472 15473 pes.data = payload.subarray(9 + payload[8]); 15474 }, 15475 15476 /** 15477 * Pass completely parsed PES packets to the next stream in the pipeline 15478 **/ 15479 flushStream = function (stream, type, forceFlush) { 15480 var packetData = new Uint8Array(stream.size), 15481 event = { 15482 type: type 15483 }, 15484 i = 0, 15485 offset = 0, 15486 packetFlushable = false, 15487 fragment; // do nothing if there is not enough buffered data for a complete 15488 // PES header 15489 15490 if (!stream.data.length || stream.size < 9) { 15491 return; 15492 } 15493 15494 event.trackId = stream.data[0].pid; // reassemble the packet 15495 15496 for (i = 0; i < stream.data.length; i++) { 15497 fragment = stream.data[i]; 15498 packetData.set(fragment.data, offset); 15499 offset += fragment.data.byteLength; 15500 } // parse assembled packet's PES header 15501 15502 15503 parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length 15504 // check that there is enough stream data to fill the packet 15505 15506 packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right 15507 15508 if (forceFlush || packetFlushable) { 15509 stream.size = 0; 15510 stream.data.length = 0; 15511 } // only emit packets that are complete. this is to avoid assembling 15512 // incomplete PES packets due to poor segmentation 15513 15514 15515 if (packetFlushable) { 15516 self.trigger('data', event); 15517 } 15518 }; 15519 15520 ElementaryStream.prototype.init.call(this); 15521 /** 15522 * Identifies M2TS packet types and parses PES packets using metadata 15523 * parsed from the PMT 15524 **/ 15525 15526 this.push = function (data) { 15527 ({ 15528 pat: function () {// we have to wait for the PMT to arrive as well before we 15529 // have any meaningful metadata 15530 }, 15531 pes: function () { 15532 var stream, streamType; 15533 15534 switch (data.streamType) { 15535 case StreamTypes$2.H264_STREAM_TYPE: 15536 stream = video; 15537 streamType = 'video'; 15538 break; 15539 15540 case StreamTypes$2.ADTS_STREAM_TYPE: 15541 stream = audio; 15542 streamType = 'audio'; 15543 break; 15544 15545 case StreamTypes$2.METADATA_STREAM_TYPE: 15546 stream = timedMetadata; 15547 streamType = 'timed-metadata'; 15548 break; 15549 15550 default: 15551 // ignore unknown stream types 15552 return; 15553 } // if a new packet is starting, we can flush the completed 15554 // packet 15555 15556 15557 if (data.payloadUnitStartIndicator) { 15558 flushStream(stream, streamType, true); 15559 } // buffer this fragment until we are sure we've received the 15560 // complete payload 15561 15562 15563 stream.data.push(data); 15564 stream.size += data.data.byteLength; 15565 }, 15566 pmt: function () { 15567 var event = { 15568 type: 'metadata', 15569 tracks: [] 15570 }; 15571 programMapTable = data.programMapTable; // translate audio and video streams to tracks 15572 15573 if (programMapTable.video !== null) { 15574 event.tracks.push({ 15575 timelineStartInfo: { 15576 baseMediaDecodeTime: 0 15577 }, 15578 id: +programMapTable.video, 15579 codec: 'avc', 15580 type: 'video' 15581 }); 15582 } 15583 15584 if (programMapTable.audio !== null) { 15585 event.tracks.push({ 15586 timelineStartInfo: { 15587 baseMediaDecodeTime: 0 15588 }, 15589 id: +programMapTable.audio, 15590 codec: 'adts', 15591 type: 'audio' 15592 }); 15593 } 15594 15595 segmentHadPmt = true; 15596 self.trigger('data', event); 15597 } 15598 })[data.type](); 15599 }; 15600 15601 this.reset = function () { 15602 video.size = 0; 15603 video.data.length = 0; 15604 audio.size = 0; 15605 audio.data.length = 0; 15606 this.trigger('reset'); 15607 }; 15608 /** 15609 * Flush any remaining input. Video PES packets may be of variable 15610 * length. Normally, the start of a new video packet can trigger the 15611 * finalization of the previous packet. That is not possible if no 15612 * more video is forthcoming, however. In that case, some other 15613 * mechanism (like the end of the file) has to be employed. When it is 15614 * clear that no additional data is forthcoming, calling this method 15615 * will flush the buffered packets. 15616 */ 15617 15618 15619 this.flushStreams_ = function () { 15620 // !!THIS ORDER IS IMPORTANT!! 15621 // video first then audio 15622 flushStream(video, 'video'); 15623 flushStream(audio, 'audio'); 15624 flushStream(timedMetadata, 'timed-metadata'); 15625 }; 15626 15627 this.flush = function () { 15628 // if on flush we haven't had a pmt emitted 15629 // and we have a pmt to emit. emit the pmt 15630 // so that we trigger a trackinfo downstream. 15631 if (!segmentHadPmt && programMapTable) { 15632 var pmt = { 15633 type: 'metadata', 15634 tracks: [] 15635 }; // translate audio and video streams to tracks 15636 15637 if (programMapTable.video !== null) { 15638 pmt.tracks.push({ 15639 timelineStartInfo: { 15640 baseMediaDecodeTime: 0 15641 }, 15642 id: +programMapTable.video, 15643 codec: 'avc', 15644 type: 'video' 15645 }); 15646 } 15647 15648 if (programMapTable.audio !== null) { 15649 pmt.tracks.push({ 15650 timelineStartInfo: { 15651 baseMediaDecodeTime: 0 15652 }, 15653 id: +programMapTable.audio, 15654 codec: 'adts', 15655 type: 'audio' 15656 }); 15657 } 15658 15659 self.trigger('data', pmt); 15660 } 15661 15662 segmentHadPmt = false; 15663 this.flushStreams_(); 15664 this.trigger('done'); 15665 }; 15666 }; 15667 15668 ElementaryStream.prototype = new Stream$4(); 15669 var m2ts$1 = { 15670 PAT_PID: 0x0000, 15671 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH$1, 15672 TransportPacketStream: TransportPacketStream, 15673 TransportParseStream: TransportParseStream, 15674 ElementaryStream: ElementaryStream, 15675 TimestampRolloverStream: TimestampRolloverStream, 15676 CaptionStream: CaptionStream$1.CaptionStream, 15677 Cea608Stream: CaptionStream$1.Cea608Stream, 15678 Cea708Stream: CaptionStream$1.Cea708Stream, 15679 MetadataStream: metadataStream 15680 }; 15681 15682 for (var type in StreamTypes$2) { 15683 if (StreamTypes$2.hasOwnProperty(type)) { 15684 m2ts$1[type] = StreamTypes$2[type]; 15685 } 15686 } 15687 15688 var m2ts_1 = m2ts$1; 15689 /** 15690 * mux.js 15691 * 15692 * Copyright (c) Brightcove 15693 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15694 */ 15695 15696 var Stream$3 = stream; 15697 var ONE_SECOND_IN_TS$2 = clock$2.ONE_SECOND_IN_TS; 15698 var AdtsStream$1; 15699 var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 15700 /* 15701 * Accepts a ElementaryStream and emits data events with parsed 15702 * AAC Audio Frames of the individual packets. Input audio in ADTS 15703 * format is unpacked and re-emitted as AAC frames. 15704 * 15705 * @see http://wiki.multimedia.cx/index.php?title=ADTS 15706 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 15707 */ 15708 15709 AdtsStream$1 = function (handlePartialSegments) { 15710 var buffer, 15711 frameNum = 0; 15712 AdtsStream$1.prototype.init.call(this); 15713 15714 this.skipWarn_ = function (start, end) { 15715 this.trigger('log', { 15716 level: 'warn', 15717 message: `adts skiping bytes ${start} to ${end} in frame ${frameNum} outside syncword` 15718 }); 15719 }; 15720 15721 this.push = function (packet) { 15722 var i = 0, 15723 frameLength, 15724 protectionSkipBytes, 15725 oldBuffer, 15726 sampleCount, 15727 adtsFrameDuration; 15728 15729 if (!handlePartialSegments) { 15730 frameNum = 0; 15731 } 15732 15733 if (packet.type !== 'audio') { 15734 // ignore non-audio data 15735 return; 15736 } // Prepend any data in the buffer to the input data so that we can parse 15737 // aac frames the cross a PES packet boundary 15738 15739 15740 if (buffer && buffer.length) { 15741 oldBuffer = buffer; 15742 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 15743 buffer.set(oldBuffer); 15744 buffer.set(packet.data, oldBuffer.byteLength); 15745 } else { 15746 buffer = packet.data; 15747 } // unpack any ADTS frames which have been fully received 15748 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 15749 15750 15751 var skip; // We use i + 7 here because we want to be able to parse the entire header. 15752 // If we don't have enough bytes to do that, then we definitely won't have a full frame. 15753 15754 while (i + 7 < buffer.length) { 15755 // Look for the start of an ADTS header.. 15756 if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) { 15757 if (typeof skip !== 'number') { 15758 skip = i; 15759 } // If a valid header was not found, jump one forward and attempt to 15760 // find a valid ADTS header starting at the next byte 15761 15762 15763 i++; 15764 continue; 15765 } 15766 15767 if (typeof skip === 'number') { 15768 this.skipWarn_(skip, i); 15769 skip = null; 15770 } // The protection skip bit tells us if we have 2 bytes of CRC data at the 15771 // end of the ADTS header 15772 15773 15774 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the 15775 // end of the sync sequence 15776 // NOTE: frame length includes the size of the header 15777 15778 frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5; 15779 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 15780 adtsFrameDuration = sampleCount * ONE_SECOND_IN_TS$2 / ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2]; // If we don't have enough data to actually finish this ADTS frame, 15781 // then we have to wait for more data 15782 15783 if (buffer.byteLength - i < frameLength) { 15784 break; 15785 } // Otherwise, deliver the complete AAC frame 15786 15787 15788 this.trigger('data', { 15789 pts: packet.pts + frameNum * adtsFrameDuration, 15790 dts: packet.dts + frameNum * adtsFrameDuration, 15791 sampleCount: sampleCount, 15792 audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1, 15793 channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6, 15794 samplerate: ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2], 15795 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 15796 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 15797 samplesize: 16, 15798 // data is the frame without it's header 15799 data: buffer.subarray(i + 7 + protectionSkipBytes, i + frameLength) 15800 }); 15801 frameNum++; 15802 i += frameLength; 15803 } 15804 15805 if (typeof skip === 'number') { 15806 this.skipWarn_(skip, i); 15807 skip = null; 15808 } // remove processed bytes from the buffer. 15809 15810 15811 buffer = buffer.subarray(i); 15812 }; 15813 15814 this.flush = function () { 15815 frameNum = 0; 15816 this.trigger('done'); 15817 }; 15818 15819 this.reset = function () { 15820 buffer = void 0; 15821 this.trigger('reset'); 15822 }; 15823 15824 this.endTimeline = function () { 15825 buffer = void 0; 15826 this.trigger('endedtimeline'); 15827 }; 15828 }; 15829 15830 AdtsStream$1.prototype = new Stream$3(); 15831 var adts = AdtsStream$1; 15832 /** 15833 * mux.js 15834 * 15835 * Copyright (c) Brightcove 15836 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15837 */ 15838 15839 var ExpGolomb$1; 15840 /** 15841 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 15842 * scheme used by h264. 15843 */ 15844 15845 ExpGolomb$1 = function (workingData) { 15846 var // the number of bytes left to examine in workingData 15847 workingBytesAvailable = workingData.byteLength, 15848 // the current word being examined 15849 workingWord = 0, 15850 // :uint 15851 // the number of bits left to examine in the current word 15852 workingBitsAvailable = 0; // :uint; 15853 // ():uint 15854 15855 this.length = function () { 15856 return 8 * workingBytesAvailable; 15857 }; // ():uint 15858 15859 15860 this.bitsAvailable = function () { 15861 return 8 * workingBytesAvailable + workingBitsAvailable; 15862 }; // ():void 15863 15864 15865 this.loadWord = function () { 15866 var position = workingData.byteLength - workingBytesAvailable, 15867 workingBytes = new Uint8Array(4), 15868 availableBytes = Math.min(4, workingBytesAvailable); 15869 15870 if (availableBytes === 0) { 15871 throw new Error('no bytes available'); 15872 } 15873 15874 workingBytes.set(workingData.subarray(position, position + availableBytes)); 15875 workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed 15876 15877 workingBitsAvailable = availableBytes * 8; 15878 workingBytesAvailable -= availableBytes; 15879 }; // (count:int):void 15880 15881 15882 this.skipBits = function (count) { 15883 var skipBytes; // :int 15884 15885 if (workingBitsAvailable > count) { 15886 workingWord <<= count; 15887 workingBitsAvailable -= count; 15888 } else { 15889 count -= workingBitsAvailable; 15890 skipBytes = Math.floor(count / 8); 15891 count -= skipBytes * 8; 15892 workingBytesAvailable -= skipBytes; 15893 this.loadWord(); 15894 workingWord <<= count; 15895 workingBitsAvailable -= count; 15896 } 15897 }; // (size:int):uint 15898 15899 15900 this.readBits = function (size) { 15901 var bits = Math.min(workingBitsAvailable, size), 15902 // :uint 15903 valu = workingWord >>> 32 - bits; // :uint 15904 // if size > 31, handle error 15905 15906 workingBitsAvailable -= bits; 15907 15908 if (workingBitsAvailable > 0) { 15909 workingWord <<= bits; 15910 } else if (workingBytesAvailable > 0) { 15911 this.loadWord(); 15912 } 15913 15914 bits = size - bits; 15915 15916 if (bits > 0) { 15917 return valu << bits | this.readBits(bits); 15918 } 15919 15920 return valu; 15921 }; // ():uint 15922 15923 15924 this.skipLeadingZeros = function () { 15925 var leadingZeroCount; // :uint 15926 15927 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) { 15928 if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) { 15929 // the first bit of working word is 1 15930 workingWord <<= leadingZeroCount; 15931 workingBitsAvailable -= leadingZeroCount; 15932 return leadingZeroCount; 15933 } 15934 } // we exhausted workingWord and still have not found a 1 15935 15936 15937 this.loadWord(); 15938 return leadingZeroCount + this.skipLeadingZeros(); 15939 }; // ():void 15940 15941 15942 this.skipUnsignedExpGolomb = function () { 15943 this.skipBits(1 + this.skipLeadingZeros()); 15944 }; // ():void 15945 15946 15947 this.skipExpGolomb = function () { 15948 this.skipBits(1 + this.skipLeadingZeros()); 15949 }; // ():uint 15950 15951 15952 this.readUnsignedExpGolomb = function () { 15953 var clz = this.skipLeadingZeros(); // :uint 15954 15955 return this.readBits(clz + 1) - 1; 15956 }; // ():int 15957 15958 15959 this.readExpGolomb = function () { 15960 var valu = this.readUnsignedExpGolomb(); // :int 15961 15962 if (0x01 & valu) { 15963 // the number is odd if the low order bit is set 15964 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 15965 } 15966 15967 return -1 * (valu >>> 1); // divide by two then make it negative 15968 }; // Some convenience functions 15969 // :Boolean 15970 15971 15972 this.readBoolean = function () { 15973 return this.readBits(1) === 1; 15974 }; // ():int 15975 15976 15977 this.readUnsignedByte = function () { 15978 return this.readBits(8); 15979 }; 15980 15981 this.loadWord(); 15982 }; 15983 15984 var expGolomb = ExpGolomb$1; 15985 /** 15986 * mux.js 15987 * 15988 * Copyright (c) Brightcove 15989 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15990 */ 15991 15992 var Stream$2 = stream; 15993 var ExpGolomb = expGolomb; 15994 var H264Stream$1, NalByteStream; 15995 var PROFILES_WITH_OPTIONAL_SPS_DATA; 15996 /** 15997 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 15998 */ 15999 16000 NalByteStream = function () { 16001 var syncPoint = 0, 16002 i, 16003 buffer; 16004 NalByteStream.prototype.init.call(this); 16005 /* 16006 * Scans a byte stream and triggers a data event with the NAL units found. 16007 * @param {Object} data Event received from H264Stream 16008 * @param {Uint8Array} data.data The h264 byte stream to be scanned 16009 * 16010 * @see H264Stream.push 16011 */ 16012 16013 this.push = function (data) { 16014 var swapBuffer; 16015 16016 if (!buffer) { 16017 buffer = data.data; 16018 } else { 16019 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 16020 swapBuffer.set(buffer); 16021 swapBuffer.set(data.data, buffer.byteLength); 16022 buffer = swapBuffer; 16023 } 16024 16025 var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B 16026 // scan for NAL unit boundaries 16027 // a match looks like this: 16028 // 0 0 1 .. NAL .. 0 0 1 16029 // ^ sync point ^ i 16030 // or this: 16031 // 0 0 1 .. NAL .. 0 0 0 16032 // ^ sync point ^ i 16033 // advance the sync point to a NAL start, if necessary 16034 16035 for (; syncPoint < len - 3; syncPoint++) { 16036 if (buffer[syncPoint + 2] === 1) { 16037 // the sync point is properly aligned 16038 i = syncPoint + 5; 16039 break; 16040 } 16041 } 16042 16043 while (i < len) { 16044 // look at the current byte to determine if we've hit the end of 16045 // a NAL unit boundary 16046 switch (buffer[i]) { 16047 case 0: 16048 // skip past non-sync sequences 16049 if (buffer[i - 1] !== 0) { 16050 i += 2; 16051 break; 16052 } else if (buffer[i - 2] !== 0) { 16053 i++; 16054 break; 16055 } // deliver the NAL unit if it isn't empty 16056 16057 16058 if (syncPoint + 3 !== i - 2) { 16059 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 16060 } // drop trailing zeroes 16061 16062 16063 do { 16064 i++; 16065 } while (buffer[i] !== 1 && i < len); 16066 16067 syncPoint = i - 2; 16068 i += 3; 16069 break; 16070 16071 case 1: 16072 // skip past non-sync sequences 16073 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 16074 i += 3; 16075 break; 16076 } // deliver the NAL unit 16077 16078 16079 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 16080 syncPoint = i - 2; 16081 i += 3; 16082 break; 16083 16084 default: 16085 // the current byte isn't a one or zero, so it cannot be part 16086 // of a sync sequence 16087 i += 3; 16088 break; 16089 } 16090 } // filter out the NAL units that were delivered 16091 16092 16093 buffer = buffer.subarray(syncPoint); 16094 i -= syncPoint; 16095 syncPoint = 0; 16096 }; 16097 16098 this.reset = function () { 16099 buffer = null; 16100 syncPoint = 0; 16101 this.trigger('reset'); 16102 }; 16103 16104 this.flush = function () { 16105 // deliver the last buffered NAL unit 16106 if (buffer && buffer.byteLength > 3) { 16107 this.trigger('data', buffer.subarray(syncPoint + 3)); 16108 } // reset the stream state 16109 16110 16111 buffer = null; 16112 syncPoint = 0; 16113 this.trigger('done'); 16114 }; 16115 16116 this.endTimeline = function () { 16117 this.flush(); 16118 this.trigger('endedtimeline'); 16119 }; 16120 }; 16121 16122 NalByteStream.prototype = new Stream$2(); // values of profile_idc that indicate additional fields are included in the SPS 16123 // see Recommendation ITU-T H.264 (4/2013), 16124 // 7.3.2.1.1 Sequence parameter set data syntax 16125 16126 PROFILES_WITH_OPTIONAL_SPS_DATA = { 16127 100: true, 16128 110: true, 16129 122: true, 16130 244: true, 16131 44: true, 16132 83: true, 16133 86: true, 16134 118: true, 16135 128: true, 16136 // TODO: the three profiles below don't 16137 // appear to have sps data in the specificiation anymore? 16138 138: true, 16139 139: true, 16140 134: true 16141 }; 16142 /** 16143 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 16144 * events. 16145 */ 16146 16147 H264Stream$1 = function () { 16148 var nalByteStream = new NalByteStream(), 16149 self, 16150 trackId, 16151 currentPts, 16152 currentDts, 16153 discardEmulationPreventionBytes, 16154 readSequenceParameterSet, 16155 skipScalingList; 16156 H264Stream$1.prototype.init.call(this); 16157 self = this; 16158 /* 16159 * Pushes a packet from a stream onto the NalByteStream 16160 * 16161 * @param {Object} packet - A packet received from a stream 16162 * @param {Uint8Array} packet.data - The raw bytes of the packet 16163 * @param {Number} packet.dts - Decode timestamp of the packet 16164 * @param {Number} packet.pts - Presentation timestamp of the packet 16165 * @param {Number} packet.trackId - The id of the h264 track this packet came from 16166 * @param {('video'|'audio')} packet.type - The type of packet 16167 * 16168 */ 16169 16170 this.push = function (packet) { 16171 if (packet.type !== 'video') { 16172 return; 16173 } 16174 16175 trackId = packet.trackId; 16176 currentPts = packet.pts; 16177 currentDts = packet.dts; 16178 nalByteStream.push(packet); 16179 }; 16180 /* 16181 * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps 16182 * for the NALUs to the next stream component. 16183 * Also, preprocess caption and sequence parameter NALUs. 16184 * 16185 * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push` 16186 * @see NalByteStream.push 16187 */ 16188 16189 16190 nalByteStream.on('data', function (data) { 16191 var event = { 16192 trackId: trackId, 16193 pts: currentPts, 16194 dts: currentDts, 16195 data: data, 16196 nalUnitTypeCode: data[0] & 0x1f 16197 }; 16198 16199 switch (event.nalUnitTypeCode) { 16200 case 0x05: 16201 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 16202 break; 16203 16204 case 0x06: 16205 event.nalUnitType = 'sei_rbsp'; 16206 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 16207 break; 16208 16209 case 0x07: 16210 event.nalUnitType = 'seq_parameter_set_rbsp'; 16211 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 16212 event.config = readSequenceParameterSet(event.escapedRBSP); 16213 break; 16214 16215 case 0x08: 16216 event.nalUnitType = 'pic_parameter_set_rbsp'; 16217 break; 16218 16219 case 0x09: 16220 event.nalUnitType = 'access_unit_delimiter_rbsp'; 16221 break; 16222 } // This triggers data on the H264Stream 16223 16224 16225 self.trigger('data', event); 16226 }); 16227 nalByteStream.on('done', function () { 16228 self.trigger('done'); 16229 }); 16230 nalByteStream.on('partialdone', function () { 16231 self.trigger('partialdone'); 16232 }); 16233 nalByteStream.on('reset', function () { 16234 self.trigger('reset'); 16235 }); 16236 nalByteStream.on('endedtimeline', function () { 16237 self.trigger('endedtimeline'); 16238 }); 16239 16240 this.flush = function () { 16241 nalByteStream.flush(); 16242 }; 16243 16244 this.partialFlush = function () { 16245 nalByteStream.partialFlush(); 16246 }; 16247 16248 this.reset = function () { 16249 nalByteStream.reset(); 16250 }; 16251 16252 this.endTimeline = function () { 16253 nalByteStream.endTimeline(); 16254 }; 16255 /** 16256 * Advance the ExpGolomb decoder past a scaling list. The scaling 16257 * list is optionally transmitted as part of a sequence parameter 16258 * set and is not relevant to transmuxing. 16259 * @param count {number} the number of entries in this scaling list 16260 * @param expGolombDecoder {object} an ExpGolomb pointed to the 16261 * start of a scaling list 16262 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 16263 */ 16264 16265 16266 skipScalingList = function (count, expGolombDecoder) { 16267 var lastScale = 8, 16268 nextScale = 8, 16269 j, 16270 deltaScale; 16271 16272 for (j = 0; j < count; j++) { 16273 if (nextScale !== 0) { 16274 deltaScale = expGolombDecoder.readExpGolomb(); 16275 nextScale = (lastScale + deltaScale + 256) % 256; 16276 } 16277 16278 lastScale = nextScale === 0 ? lastScale : nextScale; 16279 } 16280 }; 16281 /** 16282 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 16283 * Sequence Payload" 16284 * @param data {Uint8Array} the bytes of a RBSP from a NAL 16285 * unit 16286 * @return {Uint8Array} the RBSP without any Emulation 16287 * Prevention Bytes 16288 */ 16289 16290 16291 discardEmulationPreventionBytes = function (data) { 16292 var length = data.byteLength, 16293 emulationPreventionBytesPositions = [], 16294 i = 1, 16295 newLength, 16296 newData; // Find all `Emulation Prevention Bytes` 16297 16298 while (i < length - 2) { 16299 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 16300 emulationPreventionBytesPositions.push(i + 2); 16301 i += 2; 16302 } else { 16303 i++; 16304 } 16305 } // If no Emulation Prevention Bytes were found just return the original 16306 // array 16307 16308 16309 if (emulationPreventionBytesPositions.length === 0) { 16310 return data; 16311 } // Create a new array to hold the NAL unit data 16312 16313 16314 newLength = length - emulationPreventionBytesPositions.length; 16315 newData = new Uint8Array(newLength); 16316 var sourceIndex = 0; 16317 16318 for (i = 0; i < newLength; sourceIndex++, i++) { 16319 if (sourceIndex === emulationPreventionBytesPositions[0]) { 16320 // Skip this byte 16321 sourceIndex++; // Remove this position index 16322 16323 emulationPreventionBytesPositions.shift(); 16324 } 16325 16326 newData[i] = data[sourceIndex]; 16327 } 16328 16329 return newData; 16330 }; 16331 /** 16332 * Read a sequence parameter set and return some interesting video 16333 * properties. A sequence parameter set is the H264 metadata that 16334 * describes the properties of upcoming video frames. 16335 * @param data {Uint8Array} the bytes of a sequence parameter set 16336 * @return {object} an object with configuration parsed from the 16337 * sequence parameter set, including the dimensions of the 16338 * associated video frames. 16339 */ 16340 16341 16342 readSequenceParameterSet = function (data) { 16343 var frameCropLeftOffset = 0, 16344 frameCropRightOffset = 0, 16345 frameCropTopOffset = 0, 16346 frameCropBottomOffset = 0, 16347 expGolombDecoder, 16348 profileIdc, 16349 levelIdc, 16350 profileCompatibility, 16351 chromaFormatIdc, 16352 picOrderCntType, 16353 numRefFramesInPicOrderCntCycle, 16354 picWidthInMbsMinus1, 16355 picHeightInMapUnitsMinus1, 16356 frameMbsOnlyFlag, 16357 scalingListCount, 16358 sarRatio = [1, 1], 16359 aspectRatioIdc, 16360 i; 16361 expGolombDecoder = new ExpGolomb(data); 16362 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 16363 16364 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 16365 16366 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 16367 16368 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 16369 // some profiles have more optional data we don't need 16370 16371 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 16372 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 16373 16374 if (chromaFormatIdc === 3) { 16375 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 16376 } 16377 16378 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 16379 16380 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 16381 16382 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 16383 16384 if (expGolombDecoder.readBoolean()) { 16385 // seq_scaling_matrix_present_flag 16386 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 16387 16388 for (i = 0; i < scalingListCount; i++) { 16389 if (expGolombDecoder.readBoolean()) { 16390 // seq_scaling_list_present_flag[ i ] 16391 if (i < 6) { 16392 skipScalingList(16, expGolombDecoder); 16393 } else { 16394 skipScalingList(64, expGolombDecoder); 16395 } 16396 } 16397 } 16398 } 16399 } 16400 16401 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 16402 16403 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 16404 16405 if (picOrderCntType === 0) { 16406 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 16407 } else if (picOrderCntType === 1) { 16408 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 16409 16410 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 16411 16412 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 16413 16414 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 16415 16416 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 16417 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 16418 } 16419 } 16420 16421 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 16422 16423 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 16424 16425 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 16426 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 16427 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 16428 16429 if (frameMbsOnlyFlag === 0) { 16430 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 16431 } 16432 16433 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 16434 16435 if (expGolombDecoder.readBoolean()) { 16436 // frame_cropping_flag 16437 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 16438 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 16439 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 16440 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 16441 } 16442 16443 if (expGolombDecoder.readBoolean()) { 16444 // vui_parameters_present_flag 16445 if (expGolombDecoder.readBoolean()) { 16446 // aspect_ratio_info_present_flag 16447 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 16448 16449 switch (aspectRatioIdc) { 16450 case 1: 16451 sarRatio = [1, 1]; 16452 break; 16453 16454 case 2: 16455 sarRatio = [12, 11]; 16456 break; 16457 16458 case 3: 16459 sarRatio = [10, 11]; 16460 break; 16461 16462 case 4: 16463 sarRatio = [16, 11]; 16464 break; 16465 16466 case 5: 16467 sarRatio = [40, 33]; 16468 break; 16469 16470 case 6: 16471 sarRatio = [24, 11]; 16472 break; 16473 16474 case 7: 16475 sarRatio = [20, 11]; 16476 break; 16477 16478 case 8: 16479 sarRatio = [32, 11]; 16480 break; 16481 16482 case 9: 16483 sarRatio = [80, 33]; 16484 break; 16485 16486 case 10: 16487 sarRatio = [18, 11]; 16488 break; 16489 16490 case 11: 16491 sarRatio = [15, 11]; 16492 break; 16493 16494 case 12: 16495 sarRatio = [64, 33]; 16496 break; 16497 16498 case 13: 16499 sarRatio = [160, 99]; 16500 break; 16501 16502 case 14: 16503 sarRatio = [4, 3]; 16504 break; 16505 16506 case 15: 16507 sarRatio = [3, 2]; 16508 break; 16509 16510 case 16: 16511 sarRatio = [2, 1]; 16512 break; 16513 16514 case 255: 16515 { 16516 sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()]; 16517 break; 16518 } 16519 } 16520 16521 if (sarRatio) { 16522 sarRatio[0] / sarRatio[1]; 16523 } 16524 } 16525 } 16526 16527 return { 16528 profileIdc: profileIdc, 16529 levelIdc: levelIdc, 16530 profileCompatibility: profileCompatibility, 16531 width: (picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2, 16532 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2, 16533 // sar is sample aspect ratio 16534 sarRatio: sarRatio 16535 }; 16536 }; 16537 }; 16538 16539 H264Stream$1.prototype = new Stream$2(); 16540 var h264 = { 16541 H264Stream: H264Stream$1, 16542 NalByteStream: NalByteStream 16543 }; 16544 /** 16545 * mux.js 16546 * 16547 * Copyright (c) Brightcove 16548 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 16549 * 16550 * Utilities to detect basic properties and metadata about Aac data. 16551 */ 16552 16553 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 16554 16555 var parseId3TagSize = function (header, byteIndex) { 16556 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 16557 flags = header[byteIndex + 5], 16558 footerPresent = (flags & 16) >> 4; // if we get a negative returnSize clamp it to 0 16559 16560 returnSize = returnSize >= 0 ? returnSize : 0; 16561 16562 if (footerPresent) { 16563 return returnSize + 20; 16564 } 16565 16566 return returnSize + 10; 16567 }; 16568 16569 var getId3Offset = function (data, offset) { 16570 if (data.length - offset < 10 || data[offset] !== 'I'.charCodeAt(0) || data[offset + 1] !== 'D'.charCodeAt(0) || data[offset + 2] !== '3'.charCodeAt(0)) { 16571 return offset; 16572 } 16573 16574 offset += parseId3TagSize(data, offset); 16575 return getId3Offset(data, offset); 16576 }; // TODO: use vhs-utils 16577 16578 16579 var isLikelyAacData$1 = function (data) { 16580 var offset = getId3Offset(data, 0); 16581 return data.length >= offset + 2 && (data[offset] & 0xFF) === 0xFF && (data[offset + 1] & 0xF0) === 0xF0 && // verify that the 2 layer bits are 0, aka this 16582 // is not mp3 data but aac data. 16583 (data[offset + 1] & 0x16) === 0x10; 16584 }; 16585 16586 var parseSyncSafeInteger = function (data) { 16587 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 16588 }; // return a percent-encoded representation of the specified byte range 16589 // @see http://en.wikipedia.org/wiki/Percent-encoding 16590 16591 16592 var percentEncode = function (bytes, start, end) { 16593 var i, 16594 result = ''; 16595 16596 for (i = start; i < end; i++) { 16597 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 16598 } 16599 16600 return result; 16601 }; // return the string representation of the specified byte range, 16602 // interpreted as ISO-8859-1. 16603 16604 16605 var parseIso88591 = function (bytes, start, end) { 16606 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 16607 }; 16608 16609 var parseAdtsSize = function (header, byteIndex) { 16610 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 16611 middle = header[byteIndex + 4] << 3, 16612 highTwo = header[byteIndex + 3] & 0x3 << 11; 16613 return highTwo | middle | lowThree; 16614 }; 16615 16616 var parseType$5 = function (header, byteIndex) { 16617 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 16618 return 'timed-metadata'; 16619 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 16620 return 'audio'; 16621 } 16622 16623 return null; 16624 }; 16625 16626 var parseSampleRate = function (packet) { 16627 var i = 0; 16628 16629 while (i + 5 < packet.length) { 16630 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 16631 // If a valid header was not found, jump one forward and attempt to 16632 // find a valid ADTS header starting at the next byte 16633 i++; 16634 continue; 16635 } 16636 16637 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 16638 } 16639 16640 return null; 16641 }; 16642 16643 var parseAacTimestamp = function (packet) { 16644 var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag 16645 16646 frameStart = 10; 16647 16648 if (packet[5] & 0x40) { 16649 // advance the frame start past the extended header 16650 frameStart += 4; // header size field 16651 16652 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 16653 } // parse one or more ID3 frames 16654 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 16655 16656 16657 do { 16658 // determine the number of bytes in this frame 16659 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 16660 16661 if (frameSize < 1) { 16662 return null; 16663 } 16664 16665 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 16666 16667 if (frameHeader === 'PRIV') { 16668 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 16669 16670 for (var i = 0; i < frame.byteLength; i++) { 16671 if (frame[i] === 0) { 16672 var owner = parseIso88591(frame, 0, i); 16673 16674 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 16675 var d = frame.subarray(i + 1); 16676 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 16677 size *= 4; 16678 size += d[7] & 0x03; 16679 return size; 16680 } 16681 16682 break; 16683 } 16684 } 16685 } 16686 16687 frameStart += 10; // advance past the frame header 16688 16689 frameStart += frameSize; // advance past the frame body 16690 } while (frameStart < packet.byteLength); 16691 16692 return null; 16693 }; 16694 16695 var utils = { 16696 isLikelyAacData: isLikelyAacData$1, 16697 parseId3TagSize: parseId3TagSize, 16698 parseAdtsSize: parseAdtsSize, 16699 parseType: parseType$5, 16700 parseSampleRate: parseSampleRate, 16701 parseAacTimestamp: parseAacTimestamp 16702 }; 16703 /** 16704 * mux.js 16705 * 16706 * Copyright (c) Brightcove 16707 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 16708 * 16709 * A stream-based aac to mp4 converter. This utility can be used to 16710 * deliver mp4s to a SourceBuffer on platforms that support native 16711 * Media Source Extensions. 16712 */ 16713 16714 var Stream$1 = stream; 16715 var aacUtils = utils; // Constants 16716 16717 var AacStream$1; 16718 /** 16719 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 16720 */ 16721 16722 AacStream$1 = function () { 16723 var everything = new Uint8Array(), 16724 timeStamp = 0; 16725 AacStream$1.prototype.init.call(this); 16726 16727 this.setTimestamp = function (timestamp) { 16728 timeStamp = timestamp; 16729 }; 16730 16731 this.push = function (bytes) { 16732 var frameSize = 0, 16733 byteIndex = 0, 16734 bytesLeft, 16735 chunk, 16736 packet, 16737 tempLength; // If there are bytes remaining from the last segment, prepend them to the 16738 // bytes that were pushed in 16739 16740 if (everything.length) { 16741 tempLength = everything.length; 16742 everything = new Uint8Array(bytes.byteLength + tempLength); 16743 everything.set(everything.subarray(0, tempLength)); 16744 everything.set(bytes, tempLength); 16745 } else { 16746 everything = bytes; 16747 } 16748 16749 while (everything.length - byteIndex >= 3) { 16750 if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) { 16751 // Exit early because we don't have enough to parse 16752 // the ID3 tag header 16753 if (everything.length - byteIndex < 10) { 16754 break; 16755 } // check framesize 16756 16757 16758 frameSize = aacUtils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 16759 // to emit a full packet 16760 // Add to byteIndex to support multiple ID3 tags in sequence 16761 16762 if (byteIndex + frameSize > everything.length) { 16763 break; 16764 } 16765 16766 chunk = { 16767 type: 'timed-metadata', 16768 data: everything.subarray(byteIndex, byteIndex + frameSize) 16769 }; 16770 this.trigger('data', chunk); 16771 byteIndex += frameSize; 16772 continue; 16773 } else if ((everything[byteIndex] & 0xff) === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) { 16774 // Exit early because we don't have enough to parse 16775 // the ADTS frame header 16776 if (everything.length - byteIndex < 7) { 16777 break; 16778 } 16779 16780 frameSize = aacUtils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 16781 // to emit a full packet 16782 16783 if (byteIndex + frameSize > everything.length) { 16784 break; 16785 } 16786 16787 packet = { 16788 type: 'audio', 16789 data: everything.subarray(byteIndex, byteIndex + frameSize), 16790 pts: timeStamp, 16791 dts: timeStamp 16792 }; 16793 this.trigger('data', packet); 16794 byteIndex += frameSize; 16795 continue; 16796 } 16797 16798 byteIndex++; 16799 } 16800 16801 bytesLeft = everything.length - byteIndex; 16802 16803 if (bytesLeft > 0) { 16804 everything = everything.subarray(byteIndex); 16805 } else { 16806 everything = new Uint8Array(); 16807 } 16808 }; 16809 16810 this.reset = function () { 16811 everything = new Uint8Array(); 16812 this.trigger('reset'); 16813 }; 16814 16815 this.endTimeline = function () { 16816 everything = new Uint8Array(); 16817 this.trigger('endedtimeline'); 16818 }; 16819 }; 16820 16821 AacStream$1.prototype = new Stream$1(); 16822 var aac = AacStream$1; 16823 var AUDIO_PROPERTIES$1 = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize']; 16824 var audioProperties = AUDIO_PROPERTIES$1; 16825 var VIDEO_PROPERTIES$1 = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility', 'sarRatio']; 16826 var videoProperties = VIDEO_PROPERTIES$1; 16827 /** 16828 * mux.js 16829 * 16830 * Copyright (c) Brightcove 16831 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 16832 * 16833 * A stream-based mp2t to mp4 converter. This utility can be used to 16834 * deliver mp4s to a SourceBuffer on platforms that support native 16835 * Media Source Extensions. 16836 */ 16837 16838 var Stream = stream; 16839 var mp4 = mp4Generator; 16840 var frameUtils = frameUtils$1; 16841 var audioFrameUtils = audioFrameUtils$1; 16842 var trackDecodeInfo = trackDecodeInfo$1; 16843 var m2ts = m2ts_1; 16844 var clock = clock$2; 16845 var AdtsStream = adts; 16846 var H264Stream = h264.H264Stream; 16847 var AacStream = aac; 16848 var isLikelyAacData = utils.isLikelyAacData; 16849 var ONE_SECOND_IN_TS$1 = clock$2.ONE_SECOND_IN_TS; 16850 var AUDIO_PROPERTIES = audioProperties; 16851 var VIDEO_PROPERTIES = videoProperties; // object types 16852 16853 var VideoSegmentStream, AudioSegmentStream, Transmuxer, CoalesceStream; 16854 16855 var retriggerForStream = function (key, event) { 16856 event.stream = key; 16857 this.trigger('log', event); 16858 }; 16859 16860 var addPipelineLogRetriggers = function (transmuxer, pipeline) { 16861 var keys = Object.keys(pipeline); 16862 16863 for (var i = 0; i < keys.length; i++) { 16864 var key = keys[i]; // skip non-stream keys and headOfPipeline 16865 // which is just a duplicate 16866 16867 if (key === 'headOfPipeline' || !pipeline[key].on) { 16868 continue; 16869 } 16870 16871 pipeline[key].on('log', retriggerForStream.bind(transmuxer, key)); 16872 } 16873 }; 16874 /** 16875 * Compare two arrays (even typed) for same-ness 16876 */ 16877 16878 16879 var arrayEquals = function (a, b) { 16880 var i; 16881 16882 if (a.length !== b.length) { 16883 return false; 16884 } // compare the value of each element in the array 16885 16886 16887 for (i = 0; i < a.length; i++) { 16888 if (a[i] !== b[i]) { 16889 return false; 16890 } 16891 } 16892 16893 return true; 16894 }; 16895 16896 var generateSegmentTimingInfo = function (baseMediaDecodeTime, startDts, startPts, endDts, endPts, prependedContentDuration) { 16897 var ptsOffsetFromDts = startPts - startDts, 16898 decodeDuration = endDts - startDts, 16899 presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment, 16900 // however, the player time values will reflect a start from the baseMediaDecodeTime. 16901 // In order to provide relevant values for the player times, base timing info on the 16902 // baseMediaDecodeTime and the DTS and PTS durations of the segment. 16903 16904 return { 16905 start: { 16906 dts: baseMediaDecodeTime, 16907 pts: baseMediaDecodeTime + ptsOffsetFromDts 16908 }, 16909 end: { 16910 dts: baseMediaDecodeTime + decodeDuration, 16911 pts: baseMediaDecodeTime + presentationDuration 16912 }, 16913 prependedContentDuration: prependedContentDuration, 16914 baseMediaDecodeTime: baseMediaDecodeTime 16915 }; 16916 }; 16917 /** 16918 * Constructs a single-track, ISO BMFF media segment from AAC data 16919 * events. The output of this stream can be fed to a SourceBuffer 16920 * configured with a suitable initialization segment. 16921 * @param track {object} track metadata configuration 16922 * @param options {object} transmuxer options object 16923 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 16924 * in the source; false to adjust the first segment to start at 0. 16925 */ 16926 16927 16928 AudioSegmentStream = function (track, options) { 16929 var adtsFrames = [], 16930 sequenceNumber, 16931 earliestAllowedDts = 0, 16932 audioAppendStartTs = 0, 16933 videoBaseMediaDecodeTime = Infinity; 16934 options = options || {}; 16935 sequenceNumber = options.firstSequenceNumber || 0; 16936 AudioSegmentStream.prototype.init.call(this); 16937 16938 this.push = function (data) { 16939 trackDecodeInfo.collectDtsInfo(track, data); 16940 16941 if (track) { 16942 AUDIO_PROPERTIES.forEach(function (prop) { 16943 track[prop] = data[prop]; 16944 }); 16945 } // buffer audio data until end() is called 16946 16947 16948 adtsFrames.push(data); 16949 }; 16950 16951 this.setEarliestDts = function (earliestDts) { 16952 earliestAllowedDts = earliestDts; 16953 }; 16954 16955 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 16956 videoBaseMediaDecodeTime = baseMediaDecodeTime; 16957 }; 16958 16959 this.setAudioAppendStart = function (timestamp) { 16960 audioAppendStartTs = timestamp; 16961 }; 16962 16963 this.flush = function () { 16964 var frames, moof, mdat, boxes, frameDuration, segmentDuration, videoClockCyclesOfSilencePrefixed; // return early if no audio data has been observed 16965 16966 if (adtsFrames.length === 0) { 16967 this.trigger('done', 'AudioSegmentStream'); 16968 return; 16969 } 16970 16971 frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts); 16972 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); // amount of audio filled but the value is in video clock rather than audio clock 16973 16974 videoClockCyclesOfSilencePrefixed = audioFrameUtils.prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime); // we have to build the index from byte locations to 16975 // samples (that is, adts frames) in the audio data 16976 16977 track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat 16978 16979 mdat = mp4.mdat(audioFrameUtils.concatenateFrameData(frames)); 16980 adtsFrames = []; 16981 moof = mp4.moof(sequenceNumber, [track]); 16982 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time 16983 16984 sequenceNumber++; 16985 boxes.set(moof); 16986 boxes.set(mdat, moof.byteLength); 16987 trackDecodeInfo.clearDtsInfo(track); 16988 frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 * 1024 / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with 16989 // tests) on adding the timingInfo event. However, it seems unlikely that there's a 16990 // valid use-case where an init segment/data should be triggered without associated 16991 // frames. Leaving for now, but should be looked into. 16992 16993 if (frames.length) { 16994 segmentDuration = frames.length * frameDuration; 16995 this.trigger('segmentTimingInfo', generateSegmentTimingInfo( // The audio track's baseMediaDecodeTime is in audio clock cycles, but the 16996 // frame info is in video clock cycles. Convert to match expectation of 16997 // listeners (that all timestamps will be based on video clock cycles). 16998 clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate), // frame times are already in video clock, as is segment duration 16999 frames[0].dts, frames[0].pts, frames[0].dts + segmentDuration, frames[0].pts + segmentDuration, videoClockCyclesOfSilencePrefixed || 0)); 17000 this.trigger('timingInfo', { 17001 start: frames[0].pts, 17002 end: frames[0].pts + segmentDuration 17003 }); 17004 } 17005 17006 this.trigger('data', { 17007 track: track, 17008 boxes: boxes 17009 }); 17010 this.trigger('done', 'AudioSegmentStream'); 17011 }; 17012 17013 this.reset = function () { 17014 trackDecodeInfo.clearDtsInfo(track); 17015 adtsFrames = []; 17016 this.trigger('reset'); 17017 }; 17018 }; 17019 17020 AudioSegmentStream.prototype = new Stream(); 17021 /** 17022 * Constructs a single-track, ISO BMFF media segment from H264 data 17023 * events. The output of this stream can be fed to a SourceBuffer 17024 * configured with a suitable initialization segment. 17025 * @param track {object} track metadata configuration 17026 * @param options {object} transmuxer options object 17027 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 17028 * gopsToAlignWith list when attempting to align gop pts 17029 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 17030 * in the source; false to adjust the first segment to start at 0. 17031 */ 17032 17033 VideoSegmentStream = function (track, options) { 17034 var sequenceNumber, 17035 nalUnits = [], 17036 gopsToAlignWith = [], 17037 config, 17038 pps; 17039 options = options || {}; 17040 sequenceNumber = options.firstSequenceNumber || 0; 17041 VideoSegmentStream.prototype.init.call(this); 17042 delete track.minPTS; 17043 this.gopCache_ = []; 17044 /** 17045 * Constructs a ISO BMFF segment given H264 nalUnits 17046 * @param {Object} nalUnit A data event representing a nalUnit 17047 * @param {String} nalUnit.nalUnitType 17048 * @param {Object} nalUnit.config Properties for a mp4 track 17049 * @param {Uint8Array} nalUnit.data The nalUnit bytes 17050 * @see lib/codecs/h264.js 17051 **/ 17052 17053 this.push = function (nalUnit) { 17054 trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config 17055 17056 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 17057 config = nalUnit.config; 17058 track.sps = [nalUnit.data]; 17059 VIDEO_PROPERTIES.forEach(function (prop) { 17060 track[prop] = config[prop]; 17061 }, this); 17062 } 17063 17064 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 17065 pps = nalUnit.data; 17066 track.pps = [nalUnit.data]; 17067 } // buffer video until flush() is called 17068 17069 17070 nalUnits.push(nalUnit); 17071 }; 17072 /** 17073 * Pass constructed ISO BMFF track and boxes on to the 17074 * next stream in the pipeline 17075 **/ 17076 17077 17078 this.flush = function () { 17079 var frames, 17080 gopForFusion, 17081 gops, 17082 moof, 17083 mdat, 17084 boxes, 17085 prependedContentDuration = 0, 17086 firstGop, 17087 lastGop; // Throw away nalUnits at the start of the byte stream until 17088 // we find the first AUD 17089 17090 while (nalUnits.length) { 17091 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 17092 break; 17093 } 17094 17095 nalUnits.shift(); 17096 } // Return early if no video data has been observed 17097 17098 17099 if (nalUnits.length === 0) { 17100 this.resetStream_(); 17101 this.trigger('done', 'VideoSegmentStream'); 17102 return; 17103 } // Organize the raw nal-units into arrays that represent 17104 // higher-level constructs such as frames and gops 17105 // (group-of-pictures) 17106 17107 17108 frames = frameUtils.groupNalsIntoFrames(nalUnits); 17109 gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have 17110 // a problem since MSE (on Chrome) requires a leading keyframe. 17111 // 17112 // We have two approaches to repairing this situation: 17113 // 1) GOP-FUSION: 17114 // This is where we keep track of the GOPS (group-of-pictures) 17115 // from previous fragments and attempt to find one that we can 17116 // prepend to the current fragment in order to create a valid 17117 // fragment. 17118 // 2) KEYFRAME-PULLING: 17119 // Here we search for the first keyframe in the fragment and 17120 // throw away all the frames between the start of the fragment 17121 // and that keyframe. We then extend the duration and pull the 17122 // PTS of the keyframe forward so that it covers the time range 17123 // of the frames that were disposed of. 17124 // 17125 // #1 is far prefereable over #2 which can cause "stuttering" but 17126 // requires more things to be just right. 17127 17128 if (!gops[0][0].keyFrame) { 17129 // Search for a gop for fusion from our gopCache 17130 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 17131 17132 if (gopForFusion) { 17133 // in order to provide more accurate timing information about the segment, save 17134 // the number of seconds prepended to the original segment due to GOP fusion 17135 prependedContentDuration = gopForFusion.duration; 17136 gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the 17137 // new gop at the beginning 17138 17139 gops.byteLength += gopForFusion.byteLength; 17140 gops.nalCount += gopForFusion.nalCount; 17141 gops.pts = gopForFusion.pts; 17142 gops.dts = gopForFusion.dts; 17143 gops.duration += gopForFusion.duration; 17144 } else { 17145 // If we didn't find a candidate gop fall back to keyframe-pulling 17146 gops = frameUtils.extendFirstKeyFrame(gops); 17147 } 17148 } // Trim gops to align with gopsToAlignWith 17149 17150 17151 if (gopsToAlignWith.length) { 17152 var alignedGops; 17153 17154 if (options.alignGopsAtEnd) { 17155 alignedGops = this.alignGopsAtEnd_(gops); 17156 } else { 17157 alignedGops = this.alignGopsAtStart_(gops); 17158 } 17159 17160 if (!alignedGops) { 17161 // save all the nals in the last GOP into the gop cache 17162 this.gopCache_.unshift({ 17163 gop: gops.pop(), 17164 pps: track.pps, 17165 sps: track.sps 17166 }); // Keep a maximum of 6 GOPs in the cache 17167 17168 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 17169 17170 nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith 17171 17172 this.resetStream_(); 17173 this.trigger('done', 'VideoSegmentStream'); 17174 return; 17175 } // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 17176 // when recalculated before sending off to CoalesceStream 17177 17178 17179 trackDecodeInfo.clearDtsInfo(track); 17180 gops = alignedGops; 17181 } 17182 17183 trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to 17184 // samples (that is, frames) in the video data 17185 17186 track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat 17187 17188 mdat = mp4.mdat(frameUtils.concatenateNalData(gops)); 17189 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 17190 this.trigger('processedGopsInfo', gops.map(function (gop) { 17191 return { 17192 pts: gop.pts, 17193 dts: gop.dts, 17194 byteLength: gop.byteLength 17195 }; 17196 })); 17197 firstGop = gops[0]; 17198 lastGop = gops[gops.length - 1]; 17199 this.trigger('segmentTimingInfo', generateSegmentTimingInfo(track.baseMediaDecodeTime, firstGop.dts, firstGop.pts, lastGop.dts + lastGop.duration, lastGop.pts + lastGop.duration, prependedContentDuration)); 17200 this.trigger('timingInfo', { 17201 start: gops[0].pts, 17202 end: gops[gops.length - 1].pts + gops[gops.length - 1].duration 17203 }); // save all the nals in the last GOP into the gop cache 17204 17205 this.gopCache_.unshift({ 17206 gop: gops.pop(), 17207 pps: track.pps, 17208 sps: track.sps 17209 }); // Keep a maximum of 6 GOPs in the cache 17210 17211 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 17212 17213 nalUnits = []; 17214 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 17215 this.trigger('timelineStartInfo', track.timelineStartInfo); 17216 moof = mp4.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of 17217 // throwing away hundreds of media segment fragments 17218 17219 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time 17220 17221 sequenceNumber++; 17222 boxes.set(moof); 17223 boxes.set(mdat, moof.byteLength); 17224 this.trigger('data', { 17225 track: track, 17226 boxes: boxes 17227 }); 17228 this.resetStream_(); // Continue with the flush process now 17229 17230 this.trigger('done', 'VideoSegmentStream'); 17231 }; 17232 17233 this.reset = function () { 17234 this.resetStream_(); 17235 nalUnits = []; 17236 this.gopCache_.length = 0; 17237 gopsToAlignWith.length = 0; 17238 this.trigger('reset'); 17239 }; 17240 17241 this.resetStream_ = function () { 17242 trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments 17243 // for instance, when we are rendition switching 17244 17245 config = undefined; 17246 pps = undefined; 17247 }; // Search for a candidate Gop for gop-fusion from the gop cache and 17248 // return it or return null if no good candidate was found 17249 17250 17251 this.getGopForFusion_ = function (nalUnit) { 17252 var halfSecond = 45000, 17253 // Half-a-second in a 90khz clock 17254 allowableOverlap = 10000, 17255 // About 3 frames @ 30fps 17256 nearestDistance = Infinity, 17257 dtsDistance, 17258 nearestGopObj, 17259 currentGop, 17260 currentGopObj, 17261 i; // Search for the GOP nearest to the beginning of this nal unit 17262 17263 for (i = 0; i < this.gopCache_.length; i++) { 17264 currentGopObj = this.gopCache_[i]; 17265 currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS 17266 17267 if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) { 17268 continue; 17269 } // Reject Gops that would require a negative baseMediaDecodeTime 17270 17271 17272 if (currentGop.dts < track.timelineStartInfo.dts) { 17273 continue; 17274 } // The distance between the end of the gop and the start of the nalUnit 17275 17276 17277 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within 17278 // a half-second of the nal unit 17279 17280 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 17281 // Always use the closest GOP we found if there is more than 17282 // one candidate 17283 if (!nearestGopObj || nearestDistance > dtsDistance) { 17284 nearestGopObj = currentGopObj; 17285 nearestDistance = dtsDistance; 17286 } 17287 } 17288 } 17289 17290 if (nearestGopObj) { 17291 return nearestGopObj.gop; 17292 } 17293 17294 return null; 17295 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 17296 // of gopsToAlignWith starting from the START of the list 17297 17298 17299 this.alignGopsAtStart_ = function (gops) { 17300 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 17301 byteLength = gops.byteLength; 17302 nalCount = gops.nalCount; 17303 duration = gops.duration; 17304 alignIndex = gopIndex = 0; 17305 17306 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 17307 align = gopsToAlignWith[alignIndex]; 17308 gop = gops[gopIndex]; 17309 17310 if (align.pts === gop.pts) { 17311 break; 17312 } 17313 17314 if (gop.pts > align.pts) { 17315 // this current gop starts after the current gop we want to align on, so increment 17316 // align index 17317 alignIndex++; 17318 continue; 17319 } // current gop starts before the current gop we want to align on. so increment gop 17320 // index 17321 17322 17323 gopIndex++; 17324 byteLength -= gop.byteLength; 17325 nalCount -= gop.nalCount; 17326 duration -= gop.duration; 17327 } 17328 17329 if (gopIndex === 0) { 17330 // no gops to trim 17331 return gops; 17332 } 17333 17334 if (gopIndex === gops.length) { 17335 // all gops trimmed, skip appending all gops 17336 return null; 17337 } 17338 17339 alignedGops = gops.slice(gopIndex); 17340 alignedGops.byteLength = byteLength; 17341 alignedGops.duration = duration; 17342 alignedGops.nalCount = nalCount; 17343 alignedGops.pts = alignedGops[0].pts; 17344 alignedGops.dts = alignedGops[0].dts; 17345 return alignedGops; 17346 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 17347 // of gopsToAlignWith starting from the END of the list 17348 17349 17350 this.alignGopsAtEnd_ = function (gops) { 17351 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 17352 alignIndex = gopsToAlignWith.length - 1; 17353 gopIndex = gops.length - 1; 17354 alignEndIndex = null; 17355 matchFound = false; 17356 17357 while (alignIndex >= 0 && gopIndex >= 0) { 17358 align = gopsToAlignWith[alignIndex]; 17359 gop = gops[gopIndex]; 17360 17361 if (align.pts === gop.pts) { 17362 matchFound = true; 17363 break; 17364 } 17365 17366 if (align.pts > gop.pts) { 17367 alignIndex--; 17368 continue; 17369 } 17370 17371 if (alignIndex === gopsToAlignWith.length - 1) { 17372 // gop.pts is greater than the last alignment candidate. If no match is found 17373 // by the end of this loop, we still want to append gops that come after this 17374 // point 17375 alignEndIndex = gopIndex; 17376 } 17377 17378 gopIndex--; 17379 } 17380 17381 if (!matchFound && alignEndIndex === null) { 17382 return null; 17383 } 17384 17385 var trimIndex; 17386 17387 if (matchFound) { 17388 trimIndex = gopIndex; 17389 } else { 17390 trimIndex = alignEndIndex; 17391 } 17392 17393 if (trimIndex === 0) { 17394 return gops; 17395 } 17396 17397 var alignedGops = gops.slice(trimIndex); 17398 var metadata = alignedGops.reduce(function (total, gop) { 17399 total.byteLength += gop.byteLength; 17400 total.duration += gop.duration; 17401 total.nalCount += gop.nalCount; 17402 return total; 17403 }, { 17404 byteLength: 0, 17405 duration: 0, 17406 nalCount: 0 17407 }); 17408 alignedGops.byteLength = metadata.byteLength; 17409 alignedGops.duration = metadata.duration; 17410 alignedGops.nalCount = metadata.nalCount; 17411 alignedGops.pts = alignedGops[0].pts; 17412 alignedGops.dts = alignedGops[0].dts; 17413 return alignedGops; 17414 }; 17415 17416 this.alignGopsWith = function (newGopsToAlignWith) { 17417 gopsToAlignWith = newGopsToAlignWith; 17418 }; 17419 }; 17420 17421 VideoSegmentStream.prototype = new Stream(); 17422 /** 17423 * A Stream that can combine multiple streams (ie. audio & video) 17424 * into a single output segment for MSE. Also supports audio-only 17425 * and video-only streams. 17426 * @param options {object} transmuxer options object 17427 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 17428 * in the source; false to adjust the first segment to start at media timeline start. 17429 */ 17430 17431 CoalesceStream = function (options, metadataStream) { 17432 // Number of Tracks per output segment 17433 // If greater than 1, we combine multiple 17434 // tracks into a single segment 17435 this.numberOfTracks = 0; 17436 this.metadataStream = metadataStream; 17437 options = options || {}; 17438 17439 if (typeof options.remux !== 'undefined') { 17440 this.remuxTracks = !!options.remux; 17441 } else { 17442 this.remuxTracks = true; 17443 } 17444 17445 if (typeof options.keepOriginalTimestamps === 'boolean') { 17446 this.keepOriginalTimestamps = options.keepOriginalTimestamps; 17447 } else { 17448 this.keepOriginalTimestamps = false; 17449 } 17450 17451 this.pendingTracks = []; 17452 this.videoTrack = null; 17453 this.pendingBoxes = []; 17454 this.pendingCaptions = []; 17455 this.pendingMetadata = []; 17456 this.pendingBytes = 0; 17457 this.emittedTracks = 0; 17458 CoalesceStream.prototype.init.call(this); // Take output from multiple 17459 17460 this.push = function (output) { 17461 // buffer incoming captions until the associated video segment 17462 // finishes 17463 if (output.content || output.text) { 17464 return this.pendingCaptions.push(output); 17465 } // buffer incoming id3 tags until the final flush 17466 17467 17468 if (output.frames) { 17469 return this.pendingMetadata.push(output); 17470 } // Add this track to the list of pending tracks and store 17471 // important information required for the construction of 17472 // the final segment 17473 17474 17475 this.pendingTracks.push(output.track); 17476 this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome? 17477 // We unshift audio and push video because 17478 // as of Chrome 75 when switching from 17479 // one init segment to another if the video 17480 // mdat does not appear after the audio mdat 17481 // only audio will play for the duration of our transmux. 17482 17483 if (output.track.type === 'video') { 17484 this.videoTrack = output.track; 17485 this.pendingBoxes.push(output.boxes); 17486 } 17487 17488 if (output.track.type === 'audio') { 17489 this.audioTrack = output.track; 17490 this.pendingBoxes.unshift(output.boxes); 17491 } 17492 }; 17493 }; 17494 17495 CoalesceStream.prototype = new Stream(); 17496 17497 CoalesceStream.prototype.flush = function (flushSource) { 17498 var offset = 0, 17499 event = { 17500 captions: [], 17501 captionStreams: {}, 17502 metadata: [], 17503 info: {} 17504 }, 17505 caption, 17506 id3, 17507 initSegment, 17508 timelineStartPts = 0, 17509 i; 17510 17511 if (this.pendingTracks.length < this.numberOfTracks) { 17512 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 17513 // Return because we haven't received a flush from a data-generating 17514 // portion of the segment (meaning that we have only recieved meta-data 17515 // or captions.) 17516 return; 17517 } else if (this.remuxTracks) { 17518 // Return until we have enough tracks from the pipeline to remux (if we 17519 // are remuxing audio and video into a single MP4) 17520 return; 17521 } else if (this.pendingTracks.length === 0) { 17522 // In the case where we receive a flush without any data having been 17523 // received we consider it an emitted track for the purposes of coalescing 17524 // `done` events. 17525 // We do this for the case where there is an audio and video track in the 17526 // segment but no audio data. (seen in several playlists with alternate 17527 // audio tracks and no audio present in the main TS segments.) 17528 this.emittedTracks++; 17529 17530 if (this.emittedTracks >= this.numberOfTracks) { 17531 this.trigger('done'); 17532 this.emittedTracks = 0; 17533 } 17534 17535 return; 17536 } 17537 } 17538 17539 if (this.videoTrack) { 17540 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 17541 VIDEO_PROPERTIES.forEach(function (prop) { 17542 event.info[prop] = this.videoTrack[prop]; 17543 }, this); 17544 } else if (this.audioTrack) { 17545 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 17546 AUDIO_PROPERTIES.forEach(function (prop) { 17547 event.info[prop] = this.audioTrack[prop]; 17548 }, this); 17549 } 17550 17551 if (this.videoTrack || this.audioTrack) { 17552 if (this.pendingTracks.length === 1) { 17553 event.type = this.pendingTracks[0].type; 17554 } else { 17555 event.type = 'combined'; 17556 } 17557 17558 this.emittedTracks += this.pendingTracks.length; 17559 initSegment = mp4.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment 17560 17561 event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov 17562 // and track definitions 17563 17564 event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats 17565 17566 event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together 17567 17568 for (i = 0; i < this.pendingBoxes.length; i++) { 17569 event.data.set(this.pendingBoxes[i], offset); 17570 offset += this.pendingBoxes[i].byteLength; 17571 } // Translate caption PTS times into second offsets to match the 17572 // video timeline for the segment, and add track info 17573 17574 17575 for (i = 0; i < this.pendingCaptions.length; i++) { 17576 caption = this.pendingCaptions[i]; 17577 caption.startTime = clock.metadataTsToSeconds(caption.startPts, timelineStartPts, this.keepOriginalTimestamps); 17578 caption.endTime = clock.metadataTsToSeconds(caption.endPts, timelineStartPts, this.keepOriginalTimestamps); 17579 event.captionStreams[caption.stream] = true; 17580 event.captions.push(caption); 17581 } // Translate ID3 frame PTS times into second offsets to match the 17582 // video timeline for the segment 17583 17584 17585 for (i = 0; i < this.pendingMetadata.length; i++) { 17586 id3 = this.pendingMetadata[i]; 17587 id3.cueTime = clock.metadataTsToSeconds(id3.pts, timelineStartPts, this.keepOriginalTimestamps); 17588 event.metadata.push(id3); 17589 } // We add this to every single emitted segment even though we only need 17590 // it for the first 17591 17592 17593 event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state 17594 17595 this.pendingTracks.length = 0; 17596 this.videoTrack = null; 17597 this.pendingBoxes.length = 0; 17598 this.pendingCaptions.length = 0; 17599 this.pendingBytes = 0; 17600 this.pendingMetadata.length = 0; // Emit the built segment 17601 // We include captions and ID3 tags for backwards compatibility, 17602 // ideally we should send only video and audio in the data event 17603 17604 this.trigger('data', event); // Emit each caption to the outside world 17605 // Ideally, this would happen immediately on parsing captions, 17606 // but we need to ensure that video data is sent back first 17607 // so that caption timing can be adjusted to match video timing 17608 17609 for (i = 0; i < event.captions.length; i++) { 17610 caption = event.captions[i]; 17611 this.trigger('caption', caption); 17612 } // Emit each id3 tag to the outside world 17613 // Ideally, this would happen immediately on parsing the tag, 17614 // but we need to ensure that video data is sent back first 17615 // so that ID3 frame timing can be adjusted to match video timing 17616 17617 17618 for (i = 0; i < event.metadata.length; i++) { 17619 id3 = event.metadata[i]; 17620 this.trigger('id3Frame', id3); 17621 } 17622 } // Only emit `done` if all tracks have been flushed and emitted 17623 17624 17625 if (this.emittedTracks >= this.numberOfTracks) { 17626 this.trigger('done'); 17627 this.emittedTracks = 0; 17628 } 17629 }; 17630 17631 CoalesceStream.prototype.setRemux = function (val) { 17632 this.remuxTracks = val; 17633 }; 17634 /** 17635 * A Stream that expects MP2T binary data as input and produces 17636 * corresponding media segments, suitable for use with Media Source 17637 * Extension (MSE) implementations that support the ISO BMFF byte 17638 * stream format, like Chrome. 17639 */ 17640 17641 17642 Transmuxer = function (options) { 17643 var self = this, 17644 hasFlushed = true, 17645 videoTrack, 17646 audioTrack; 17647 Transmuxer.prototype.init.call(this); 17648 options = options || {}; 17649 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 17650 this.transmuxPipeline_ = {}; 17651 17652 this.setupAacPipeline = function () { 17653 var pipeline = {}; 17654 this.transmuxPipeline_ = pipeline; 17655 pipeline.type = 'aac'; 17656 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 17657 17658 pipeline.aacStream = new AacStream(); 17659 pipeline.audioTimestampRolloverStream = new m2ts.TimestampRolloverStream('audio'); 17660 pipeline.timedMetadataTimestampRolloverStream = new m2ts.TimestampRolloverStream('timed-metadata'); 17661 pipeline.adtsStream = new AdtsStream(); 17662 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 17663 pipeline.headOfPipeline = pipeline.aacStream; 17664 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 17665 pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 17666 pipeline.metadataStream.on('timestamp', function (frame) { 17667 pipeline.aacStream.setTimestamp(frame.timeStamp); 17668 }); 17669 pipeline.aacStream.on('data', function (data) { 17670 if (data.type !== 'timed-metadata' && data.type !== 'audio' || pipeline.audioSegmentStream) { 17671 return; 17672 } 17673 17674 audioTrack = audioTrack || { 17675 timelineStartInfo: { 17676 baseMediaDecodeTime: self.baseMediaDecodeTime 17677 }, 17678 codec: 'adts', 17679 type: 'audio' 17680 }; // hook up the audio segment stream to the first track with aac data 17681 17682 pipeline.coalesceStream.numberOfTracks++; 17683 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 17684 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 17685 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline 17686 17687 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); // emit pmt info 17688 17689 self.trigger('trackinfo', { 17690 hasAudio: !!audioTrack, 17691 hasVideo: !!videoTrack 17692 }); 17693 }); // Re-emit any data coming from the coalesce stream to the outside world 17694 17695 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline 17696 17697 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 17698 addPipelineLogRetriggers(this, pipeline); 17699 }; 17700 17701 this.setupTsPipeline = function () { 17702 var pipeline = {}; 17703 this.transmuxPipeline_ = pipeline; 17704 pipeline.type = 'ts'; 17705 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 17706 17707 pipeline.packetStream = new m2ts.TransportPacketStream(); 17708 pipeline.parseStream = new m2ts.TransportParseStream(); 17709 pipeline.elementaryStream = new m2ts.ElementaryStream(); 17710 pipeline.timestampRolloverStream = new m2ts.TimestampRolloverStream(); 17711 pipeline.adtsStream = new AdtsStream(); 17712 pipeline.h264Stream = new H264Stream(); 17713 pipeline.captionStream = new m2ts.CaptionStream(options); 17714 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 17715 pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams 17716 17717 pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream).pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!! 17718 // demux the streams 17719 17720 pipeline.timestampRolloverStream.pipe(pipeline.h264Stream); 17721 pipeline.timestampRolloverStream.pipe(pipeline.adtsStream); 17722 pipeline.timestampRolloverStream.pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream 17723 17724 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 17725 pipeline.elementaryStream.on('data', function (data) { 17726 var i; 17727 17728 if (data.type === 'metadata') { 17729 i = data.tracks.length; // scan the tracks listed in the metadata 17730 17731 while (i--) { 17732 if (!videoTrack && data.tracks[i].type === 'video') { 17733 videoTrack = data.tracks[i]; 17734 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 17735 } else if (!audioTrack && data.tracks[i].type === 'audio') { 17736 audioTrack = data.tracks[i]; 17737 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 17738 } 17739 } // hook up the video segment stream to the first track with h264 data 17740 17741 17742 if (videoTrack && !pipeline.videoSegmentStream) { 17743 pipeline.coalesceStream.numberOfTracks++; 17744 pipeline.videoSegmentStream = new VideoSegmentStream(videoTrack, options); 17745 pipeline.videoSegmentStream.on('log', self.getLogTrigger_('videoSegmentStream')); 17746 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 17747 // When video emits timelineStartInfo data after a flush, we forward that 17748 // info to the AudioSegmentStream, if it exists, because video timeline 17749 // data takes precedence. Do not do this if keepOriginalTimestamps is set, 17750 // because this is a particularly subtle form of timestamp alteration. 17751 if (audioTrack && !options.keepOriginalTimestamps) { 17752 audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the 17753 // very earliest DTS we have seen in video because Chrome will 17754 // interpret any video track with a baseMediaDecodeTime that is 17755 // non-zero as a gap. 17756 17757 pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts - self.baseMediaDecodeTime); 17758 } 17759 }); 17760 pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo')); 17761 pipeline.videoSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'videoSegmentTimingInfo')); 17762 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 17763 if (audioTrack) { 17764 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 17765 } 17766 }); 17767 pipeline.videoSegmentStream.on('timingInfo', self.trigger.bind(self, 'videoTimingInfo')); // Set up the final part of the video pipeline 17768 17769 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 17770 } 17771 17772 if (audioTrack && !pipeline.audioSegmentStream) { 17773 // hook up the audio segment stream to the first track with aac data 17774 pipeline.coalesceStream.numberOfTracks++; 17775 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 17776 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 17777 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); 17778 pipeline.audioSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'audioSegmentTimingInfo')); // Set up the final part of the audio pipeline 17779 17780 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 17781 } // emit pmt info 17782 17783 17784 self.trigger('trackinfo', { 17785 hasAudio: !!audioTrack, 17786 hasVideo: !!videoTrack 17787 }); 17788 } 17789 }); // Re-emit any data coming from the coalesce stream to the outside world 17790 17791 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 17792 pipeline.coalesceStream.on('id3Frame', function (id3Frame) { 17793 id3Frame.dispatchType = pipeline.metadataStream.dispatchType; 17794 self.trigger('id3Frame', id3Frame); 17795 }); 17796 pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline 17797 17798 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 17799 addPipelineLogRetriggers(this, pipeline); 17800 }; // hook up the segment streams once track metadata is delivered 17801 17802 17803 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 17804 var pipeline = this.transmuxPipeline_; 17805 17806 if (!options.keepOriginalTimestamps) { 17807 this.baseMediaDecodeTime = baseMediaDecodeTime; 17808 } 17809 17810 if (audioTrack) { 17811 audioTrack.timelineStartInfo.dts = undefined; 17812 audioTrack.timelineStartInfo.pts = undefined; 17813 trackDecodeInfo.clearDtsInfo(audioTrack); 17814 17815 if (pipeline.audioTimestampRolloverStream) { 17816 pipeline.audioTimestampRolloverStream.discontinuity(); 17817 } 17818 } 17819 17820 if (videoTrack) { 17821 if (pipeline.videoSegmentStream) { 17822 pipeline.videoSegmentStream.gopCache_ = []; 17823 } 17824 17825 videoTrack.timelineStartInfo.dts = undefined; 17826 videoTrack.timelineStartInfo.pts = undefined; 17827 trackDecodeInfo.clearDtsInfo(videoTrack); 17828 pipeline.captionStream.reset(); 17829 } 17830 17831 if (pipeline.timestampRolloverStream) { 17832 pipeline.timestampRolloverStream.discontinuity(); 17833 } 17834 }; 17835 17836 this.setAudioAppendStart = function (timestamp) { 17837 if (audioTrack) { 17838 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 17839 } 17840 }; 17841 17842 this.setRemux = function (val) { 17843 var pipeline = this.transmuxPipeline_; 17844 options.remux = val; 17845 17846 if (pipeline && pipeline.coalesceStream) { 17847 pipeline.coalesceStream.setRemux(val); 17848 } 17849 }; 17850 17851 this.alignGopsWith = function (gopsToAlignWith) { 17852 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 17853 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 17854 } 17855 }; 17856 17857 this.getLogTrigger_ = function (key) { 17858 var self = this; 17859 return function (event) { 17860 event.stream = key; 17861 self.trigger('log', event); 17862 }; 17863 }; // feed incoming data to the front of the parsing pipeline 17864 17865 17866 this.push = function (data) { 17867 if (hasFlushed) { 17868 var isAac = isLikelyAacData(data); 17869 17870 if (isAac && this.transmuxPipeline_.type !== 'aac') { 17871 this.setupAacPipeline(); 17872 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 17873 this.setupTsPipeline(); 17874 } 17875 17876 hasFlushed = false; 17877 } 17878 17879 this.transmuxPipeline_.headOfPipeline.push(data); 17880 }; // flush any buffered data 17881 17882 17883 this.flush = function () { 17884 hasFlushed = true; // Start at the top of the pipeline and flush all pending work 17885 17886 this.transmuxPipeline_.headOfPipeline.flush(); 17887 }; 17888 17889 this.endTimeline = function () { 17890 this.transmuxPipeline_.headOfPipeline.endTimeline(); 17891 }; 17892 17893 this.reset = function () { 17894 if (this.transmuxPipeline_.headOfPipeline) { 17895 this.transmuxPipeline_.headOfPipeline.reset(); 17896 } 17897 }; // Caption data has to be reset when seeking outside buffered range 17898 17899 17900 this.resetCaptions = function () { 17901 if (this.transmuxPipeline_.captionStream) { 17902 this.transmuxPipeline_.captionStream.reset(); 17903 } 17904 }; 17905 }; 17906 17907 Transmuxer.prototype = new Stream(); 17908 var transmuxer = { 17909 Transmuxer: Transmuxer, 17910 VideoSegmentStream: VideoSegmentStream, 17911 AudioSegmentStream: AudioSegmentStream, 17912 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 17913 VIDEO_PROPERTIES: VIDEO_PROPERTIES, 17914 // exported for testing 17915 generateSegmentTimingInfo: generateSegmentTimingInfo 17916 }; 17917 /** 17918 * mux.js 17919 * 17920 * Copyright (c) Brightcove 17921 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 17922 */ 17923 17924 var toUnsigned$3 = function (value) { 17925 return value >>> 0; 17926 }; 17927 17928 var toHexString$1 = function (value) { 17929 return ('00' + value.toString(16)).slice(-2); 17930 }; 17931 17932 var bin = { 17933 toUnsigned: toUnsigned$3, 17934 toHexString: toHexString$1 17935 }; 17936 17937 var parseType$4 = function (buffer) { 17938 var result = ''; 17939 result += String.fromCharCode(buffer[0]); 17940 result += String.fromCharCode(buffer[1]); 17941 result += String.fromCharCode(buffer[2]); 17942 result += String.fromCharCode(buffer[3]); 17943 return result; 17944 }; 17945 17946 var parseType_1 = parseType$4; 17947 var toUnsigned$2 = bin.toUnsigned; 17948 var parseType$3 = parseType_1; 17949 17950 var findBox$5 = function (data, path) { 17951 var results = [], 17952 i, 17953 size, 17954 type, 17955 end, 17956 subresults; 17957 17958 if (!path.length) { 17959 // short-circuit the search for empty paths 17960 return null; 17961 } 17962 17963 for (i = 0; i < data.byteLength;) { 17964 size = toUnsigned$2(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]); 17965 type = parseType$3(data.subarray(i + 4, i + 8)); 17966 end = size > 1 ? i + size : data.byteLength; 17967 17968 if (type === path[0]) { 17969 if (path.length === 1) { 17970 // this is the end of the path and we've found the box we were 17971 // looking for 17972 results.push(data.subarray(i + 8, end)); 17973 } else { 17974 // recursively search for the next box along the path 17975 subresults = findBox$5(data.subarray(i + 8, end), path.slice(1)); 17976 17977 if (subresults.length) { 17978 results = results.concat(subresults); 17979 } 17980 } 17981 } 17982 17983 i = end; 17984 } // we've finished searching all of data 17985 17986 17987 return results; 17988 }; 17989 17990 var findBox_1 = findBox$5; 17991 var toUnsigned$1 = bin.toUnsigned; 17992 var getUint64$4 = numbers.getUint64; 17993 17994 var tfdt = function (data) { 17995 var result = { 17996 version: data[0], 17997 flags: new Uint8Array(data.subarray(1, 4)) 17998 }; 17999 18000 if (result.version === 1) { 18001 result.baseMediaDecodeTime = getUint64$4(data.subarray(4)); 18002 } else { 18003 result.baseMediaDecodeTime = toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]); 18004 } 18005 18006 return result; 18007 }; 18008 18009 var parseTfdt$3 = tfdt; 18010 18011 var tfhd = function (data) { 18012 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18013 result = { 18014 version: data[0], 18015 flags: new Uint8Array(data.subarray(1, 4)), 18016 trackId: view.getUint32(4) 18017 }, 18018 baseDataOffsetPresent = result.flags[2] & 0x01, 18019 sampleDescriptionIndexPresent = result.flags[2] & 0x02, 18020 defaultSampleDurationPresent = result.flags[2] & 0x08, 18021 defaultSampleSizePresent = result.flags[2] & 0x10, 18022 defaultSampleFlagsPresent = result.flags[2] & 0x20, 18023 durationIsEmpty = result.flags[0] & 0x010000, 18024 defaultBaseIsMoof = result.flags[0] & 0x020000, 18025 i; 18026 i = 8; 18027 18028 if (baseDataOffsetPresent) { 18029 i += 4; // truncate top 4 bytes 18030 // FIXME: should we read the full 64 bits? 18031 18032 result.baseDataOffset = view.getUint32(12); 18033 i += 4; 18034 } 18035 18036 if (sampleDescriptionIndexPresent) { 18037 result.sampleDescriptionIndex = view.getUint32(i); 18038 i += 4; 18039 } 18040 18041 if (defaultSampleDurationPresent) { 18042 result.defaultSampleDuration = view.getUint32(i); 18043 i += 4; 18044 } 18045 18046 if (defaultSampleSizePresent) { 18047 result.defaultSampleSize = view.getUint32(i); 18048 i += 4; 18049 } 18050 18051 if (defaultSampleFlagsPresent) { 18052 result.defaultSampleFlags = view.getUint32(i); 18053 } 18054 18055 if (durationIsEmpty) { 18056 result.durationIsEmpty = true; 18057 } 18058 18059 if (!baseDataOffsetPresent && defaultBaseIsMoof) { 18060 result.baseDataOffsetIsMoof = true; 18061 } 18062 18063 return result; 18064 }; 18065 18066 var parseTfhd$2 = tfhd; 18067 var getUint64$3 = numbers.getUint64; 18068 18069 var parseSidx = function (data) { 18070 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18071 result = { 18072 version: data[0], 18073 flags: new Uint8Array(data.subarray(1, 4)), 18074 references: [], 18075 referenceId: view.getUint32(4), 18076 timescale: view.getUint32(8) 18077 }, 18078 i = 12; 18079 18080 if (result.version === 0) { 18081 result.earliestPresentationTime = view.getUint32(i); 18082 result.firstOffset = view.getUint32(i + 4); 18083 i += 8; 18084 } else { 18085 // read 64 bits 18086 result.earliestPresentationTime = getUint64$3(data.subarray(i)); 18087 result.firstOffset = getUint64$3(data.subarray(i + 8)); 18088 i += 16; 18089 } 18090 18091 i += 2; // reserved 18092 18093 var referenceCount = view.getUint16(i); 18094 i += 2; // start of references 18095 18096 for (; referenceCount > 0; i += 12, referenceCount--) { 18097 result.references.push({ 18098 referenceType: (data[i] & 0x80) >>> 7, 18099 referencedSize: view.getUint32(i) & 0x7FFFFFFF, 18100 subsegmentDuration: view.getUint32(i + 4), 18101 startsWithSap: !!(data[i + 8] & 0x80), 18102 sapType: (data[i + 8] & 0x70) >>> 4, 18103 sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF 18104 }); 18105 } 18106 18107 return result; 18108 }; 18109 18110 var parseSidx_1 = parseSidx; 18111 18112 var parseSampleFlags$1 = function (flags) { 18113 return { 18114 isLeading: (flags[0] & 0x0c) >>> 2, 18115 dependsOn: flags[0] & 0x03, 18116 isDependedOn: (flags[1] & 0xc0) >>> 6, 18117 hasRedundancy: (flags[1] & 0x30) >>> 4, 18118 paddingValue: (flags[1] & 0x0e) >>> 1, 18119 isNonSyncSample: flags[1] & 0x01, 18120 degradationPriority: flags[2] << 8 | flags[3] 18121 }; 18122 }; 18123 18124 var parseSampleFlags_1 = parseSampleFlags$1; 18125 var parseSampleFlags = parseSampleFlags_1; 18126 18127 var trun = function (data) { 18128 var result = { 18129 version: data[0], 18130 flags: new Uint8Array(data.subarray(1, 4)), 18131 samples: [] 18132 }, 18133 view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18134 // Flag interpretation 18135 dataOffsetPresent = result.flags[2] & 0x01, 18136 // compare with 2nd byte of 0x1 18137 firstSampleFlagsPresent = result.flags[2] & 0x04, 18138 // compare with 2nd byte of 0x4 18139 sampleDurationPresent = result.flags[1] & 0x01, 18140 // compare with 2nd byte of 0x100 18141 sampleSizePresent = result.flags[1] & 0x02, 18142 // compare with 2nd byte of 0x200 18143 sampleFlagsPresent = result.flags[1] & 0x04, 18144 // compare with 2nd byte of 0x400 18145 sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, 18146 // compare with 2nd byte of 0x800 18147 sampleCount = view.getUint32(4), 18148 offset = 8, 18149 sample; 18150 18151 if (dataOffsetPresent) { 18152 // 32 bit signed integer 18153 result.dataOffset = view.getInt32(offset); 18154 offset += 4; 18155 } // Overrides the flags for the first sample only. The order of 18156 // optional values will be: duration, size, compositionTimeOffset 18157 18158 18159 if (firstSampleFlagsPresent && sampleCount) { 18160 sample = { 18161 flags: parseSampleFlags(data.subarray(offset, offset + 4)) 18162 }; 18163 offset += 4; 18164 18165 if (sampleDurationPresent) { 18166 sample.duration = view.getUint32(offset); 18167 offset += 4; 18168 } 18169 18170 if (sampleSizePresent) { 18171 sample.size = view.getUint32(offset); 18172 offset += 4; 18173 } 18174 18175 if (sampleCompositionTimeOffsetPresent) { 18176 if (result.version === 1) { 18177 sample.compositionTimeOffset = view.getInt32(offset); 18178 } else { 18179 sample.compositionTimeOffset = view.getUint32(offset); 18180 } 18181 18182 offset += 4; 18183 } 18184 18185 result.samples.push(sample); 18186 sampleCount--; 18187 } 18188 18189 while (sampleCount--) { 18190 sample = {}; 18191 18192 if (sampleDurationPresent) { 18193 sample.duration = view.getUint32(offset); 18194 offset += 4; 18195 } 18196 18197 if (sampleSizePresent) { 18198 sample.size = view.getUint32(offset); 18199 offset += 4; 18200 } 18201 18202 if (sampleFlagsPresent) { 18203 sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); 18204 offset += 4; 18205 } 18206 18207 if (sampleCompositionTimeOffsetPresent) { 18208 if (result.version === 1) { 18209 sample.compositionTimeOffset = view.getInt32(offset); 18210 } else { 18211 sample.compositionTimeOffset = view.getUint32(offset); 18212 } 18213 18214 offset += 4; 18215 } 18216 18217 result.samples.push(sample); 18218 } 18219 18220 return result; 18221 }; 18222 18223 var parseTrun$2 = trun; 18224 /** 18225 * mux.js 18226 * 18227 * Copyright (c) Brightcove 18228 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 18229 * 18230 * Parse the internal MP4 structure into an equivalent javascript 18231 * object. 18232 */ 18233 18234 var numberHelpers = numbers; 18235 var getUint64$2 = numberHelpers.getUint64; 18236 18237 var inspectMp4, 18238 textifyMp4, 18239 parseMp4Date = function (seconds) { 18240 return new Date(seconds * 1000 - 2082844800000); 18241 }, 18242 parseType$2 = parseType_1, 18243 findBox$4 = findBox_1, 18244 nalParse = function (avcStream) { 18245 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 18246 result = [], 18247 i, 18248 length; 18249 18250 for (i = 0; i + 4 < avcStream.length; i += length) { 18251 length = avcView.getUint32(i); 18252 i += 4; // bail if this doesn't appear to be an H264 stream 18253 18254 if (length <= 0) { 18255 result.push('<span style=\'color:red;\'>MALFORMED DATA</span>'); 18256 continue; 18257 } 18258 18259 switch (avcStream[i] & 0x1F) { 18260 case 0x01: 18261 result.push('slice_layer_without_partitioning_rbsp'); 18262 break; 18263 18264 case 0x05: 18265 result.push('slice_layer_without_partitioning_rbsp_idr'); 18266 break; 18267 18268 case 0x06: 18269 result.push('sei_rbsp'); 18270 break; 18271 18272 case 0x07: 18273 result.push('seq_parameter_set_rbsp'); 18274 break; 18275 18276 case 0x08: 18277 result.push('pic_parameter_set_rbsp'); 18278 break; 18279 18280 case 0x09: 18281 result.push('access_unit_delimiter_rbsp'); 18282 break; 18283 18284 default: 18285 result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F); 18286 break; 18287 } 18288 } 18289 18290 return result; 18291 }, 18292 // registry of handlers for individual mp4 box types 18293 parse = { 18294 // codingname, not a first-class box type. stsd entries share the 18295 // same format as real boxes so the parsing infrastructure can be 18296 // shared 18297 avc1: function (data) { 18298 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 18299 return { 18300 dataReferenceIndex: view.getUint16(6), 18301 width: view.getUint16(24), 18302 height: view.getUint16(26), 18303 horizresolution: view.getUint16(28) + view.getUint16(30) / 16, 18304 vertresolution: view.getUint16(32) + view.getUint16(34) / 16, 18305 frameCount: view.getUint16(40), 18306 depth: view.getUint16(74), 18307 config: inspectMp4(data.subarray(78, data.byteLength)) 18308 }; 18309 }, 18310 avcC: function (data) { 18311 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18312 result = { 18313 configurationVersion: data[0], 18314 avcProfileIndication: data[1], 18315 profileCompatibility: data[2], 18316 avcLevelIndication: data[3], 18317 lengthSizeMinusOne: data[4] & 0x03, 18318 sps: [], 18319 pps: [] 18320 }, 18321 numOfSequenceParameterSets = data[5] & 0x1f, 18322 numOfPictureParameterSets, 18323 nalSize, 18324 offset, 18325 i; // iterate past any SPSs 18326 18327 offset = 6; 18328 18329 for (i = 0; i < numOfSequenceParameterSets; i++) { 18330 nalSize = view.getUint16(offset); 18331 offset += 2; 18332 result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); 18333 offset += nalSize; 18334 } // iterate past any PPSs 18335 18336 18337 numOfPictureParameterSets = data[offset]; 18338 offset++; 18339 18340 for (i = 0; i < numOfPictureParameterSets; i++) { 18341 nalSize = view.getUint16(offset); 18342 offset += 2; 18343 result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); 18344 offset += nalSize; 18345 } 18346 18347 return result; 18348 }, 18349 btrt: function (data) { 18350 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 18351 return { 18352 bufferSizeDB: view.getUint32(0), 18353 maxBitrate: view.getUint32(4), 18354 avgBitrate: view.getUint32(8) 18355 }; 18356 }, 18357 edts: function edts(data) { 18358 return { 18359 boxes: inspectMp4(data) 18360 }; 18361 }, 18362 elst: function elst(data) { 18363 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18364 result = { 18365 version: view.getUint8(0), 18366 flags: new Uint8Array(data.subarray(1, 4)), 18367 edits: [] 18368 }, 18369 entryCount = view.getUint32(4), 18370 i; 18371 18372 for (i = 8; entryCount; entryCount--) { 18373 if (result.version === 0) { 18374 result.edits.push({ 18375 segmentDuration: view.getUint32(i), 18376 mediaTime: view.getInt32(i + 4), 18377 mediaRate: view.getUint16(i + 8) + view.getUint16(i + 10) / (256 * 256) 18378 }); 18379 i += 12; 18380 } else { 18381 result.edits.push({ 18382 segmentDuration: getUint64$2(data.subarray(i)), 18383 mediaTime: getUint64$2(data.subarray(i + 8)), 18384 mediaRate: view.getUint16(i + 16) + view.getUint16(i + 18) / (256 * 256) 18385 }); 18386 i += 20; 18387 } 18388 } 18389 18390 return result; 18391 }, 18392 esds: function (data) { 18393 return { 18394 version: data[0], 18395 flags: new Uint8Array(data.subarray(1, 4)), 18396 esId: data[6] << 8 | data[7], 18397 streamPriority: data[8] & 0x1f, 18398 decoderConfig: { 18399 objectProfileIndication: data[11], 18400 streamType: data[12] >>> 2 & 0x3f, 18401 bufferSize: data[13] << 16 | data[14] << 8 | data[15], 18402 maxBitrate: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19], 18403 avgBitrate: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23], 18404 decoderConfigDescriptor: { 18405 tag: data[24], 18406 length: data[25], 18407 audioObjectType: data[26] >>> 3 & 0x1f, 18408 samplingFrequencyIndex: (data[26] & 0x07) << 1 | data[27] >>> 7 & 0x01, 18409 channelConfiguration: data[27] >>> 3 & 0x0f 18410 } 18411 } 18412 }; 18413 }, 18414 ftyp: function (data) { 18415 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18416 result = { 18417 majorBrand: parseType$2(data.subarray(0, 4)), 18418 minorVersion: view.getUint32(4), 18419 compatibleBrands: [] 18420 }, 18421 i = 8; 18422 18423 while (i < data.byteLength) { 18424 result.compatibleBrands.push(parseType$2(data.subarray(i, i + 4))); 18425 i += 4; 18426 } 18427 18428 return result; 18429 }, 18430 dinf: function (data) { 18431 return { 18432 boxes: inspectMp4(data) 18433 }; 18434 }, 18435 dref: function (data) { 18436 return { 18437 version: data[0], 18438 flags: new Uint8Array(data.subarray(1, 4)), 18439 dataReferences: inspectMp4(data.subarray(8)) 18440 }; 18441 }, 18442 hdlr: function (data) { 18443 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18444 result = { 18445 version: view.getUint8(0), 18446 flags: new Uint8Array(data.subarray(1, 4)), 18447 handlerType: parseType$2(data.subarray(8, 12)), 18448 name: '' 18449 }, 18450 i = 8; // parse out the name field 18451 18452 for (i = 24; i < data.byteLength; i++) { 18453 if (data[i] === 0x00) { 18454 // the name field is null-terminated 18455 i++; 18456 break; 18457 } 18458 18459 result.name += String.fromCharCode(data[i]); 18460 } // decode UTF-8 to javascript's internal representation 18461 // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html 18462 18463 18464 result.name = decodeURIComponent(escape(result.name)); 18465 return result; 18466 }, 18467 mdat: function (data) { 18468 return { 18469 byteLength: data.byteLength, 18470 nals: nalParse(data) 18471 }; 18472 }, 18473 mdhd: function (data) { 18474 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18475 i = 4, 18476 language, 18477 result = { 18478 version: view.getUint8(0), 18479 flags: new Uint8Array(data.subarray(1, 4)), 18480 language: '' 18481 }; 18482 18483 if (result.version === 1) { 18484 i += 4; 18485 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 18486 18487 i += 8; 18488 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 18489 18490 i += 4; 18491 result.timescale = view.getUint32(i); 18492 i += 8; 18493 result.duration = view.getUint32(i); // truncating top 4 bytes 18494 } else { 18495 result.creationTime = parseMp4Date(view.getUint32(i)); 18496 i += 4; 18497 result.modificationTime = parseMp4Date(view.getUint32(i)); 18498 i += 4; 18499 result.timescale = view.getUint32(i); 18500 i += 4; 18501 result.duration = view.getUint32(i); 18502 } 18503 18504 i += 4; // language is stored as an ISO-639-2/T code in an array of three 5-bit fields 18505 // each field is the packed difference between its ASCII value and 0x60 18506 18507 language = view.getUint16(i); 18508 result.language += String.fromCharCode((language >> 10) + 0x60); 18509 result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60); 18510 result.language += String.fromCharCode((language & 0x1f) + 0x60); 18511 return result; 18512 }, 18513 mdia: function (data) { 18514 return { 18515 boxes: inspectMp4(data) 18516 }; 18517 }, 18518 mfhd: function (data) { 18519 return { 18520 version: data[0], 18521 flags: new Uint8Array(data.subarray(1, 4)), 18522 sequenceNumber: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7] 18523 }; 18524 }, 18525 minf: function (data) { 18526 return { 18527 boxes: inspectMp4(data) 18528 }; 18529 }, 18530 // codingname, not a first-class box type. stsd entries share the 18531 // same format as real boxes so the parsing infrastructure can be 18532 // shared 18533 mp4a: function (data) { 18534 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18535 result = { 18536 // 6 bytes reserved 18537 dataReferenceIndex: view.getUint16(6), 18538 // 4 + 4 bytes reserved 18539 channelcount: view.getUint16(16), 18540 samplesize: view.getUint16(18), 18541 // 2 bytes pre_defined 18542 // 2 bytes reserved 18543 samplerate: view.getUint16(24) + view.getUint16(26) / 65536 18544 }; // if there are more bytes to process, assume this is an ISO/IEC 18545 // 14496-14 MP4AudioSampleEntry and parse the ESDBox 18546 18547 if (data.byteLength > 28) { 18548 result.streamDescriptor = inspectMp4(data.subarray(28))[0]; 18549 } 18550 18551 return result; 18552 }, 18553 moof: function (data) { 18554 return { 18555 boxes: inspectMp4(data) 18556 }; 18557 }, 18558 moov: function (data) { 18559 return { 18560 boxes: inspectMp4(data) 18561 }; 18562 }, 18563 mvex: function (data) { 18564 return { 18565 boxes: inspectMp4(data) 18566 }; 18567 }, 18568 mvhd: function (data) { 18569 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18570 i = 4, 18571 result = { 18572 version: view.getUint8(0), 18573 flags: new Uint8Array(data.subarray(1, 4)) 18574 }; 18575 18576 if (result.version === 1) { 18577 i += 4; 18578 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 18579 18580 i += 8; 18581 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 18582 18583 i += 4; 18584 result.timescale = view.getUint32(i); 18585 i += 8; 18586 result.duration = view.getUint32(i); // truncating top 4 bytes 18587 } else { 18588 result.creationTime = parseMp4Date(view.getUint32(i)); 18589 i += 4; 18590 result.modificationTime = parseMp4Date(view.getUint32(i)); 18591 i += 4; 18592 result.timescale = view.getUint32(i); 18593 i += 4; 18594 result.duration = view.getUint32(i); 18595 } 18596 18597 i += 4; // convert fixed-point, base 16 back to a number 18598 18599 result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16; 18600 i += 4; 18601 result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8; 18602 i += 2; 18603 i += 2; 18604 i += 2 * 4; 18605 result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4)); 18606 i += 9 * 4; 18607 i += 6 * 4; 18608 result.nextTrackId = view.getUint32(i); 18609 return result; 18610 }, 18611 pdin: function (data) { 18612 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 18613 return { 18614 version: view.getUint8(0), 18615 flags: new Uint8Array(data.subarray(1, 4)), 18616 rate: view.getUint32(4), 18617 initialDelay: view.getUint32(8) 18618 }; 18619 }, 18620 sdtp: function (data) { 18621 var result = { 18622 version: data[0], 18623 flags: new Uint8Array(data.subarray(1, 4)), 18624 samples: [] 18625 }, 18626 i; 18627 18628 for (i = 4; i < data.byteLength; i++) { 18629 result.samples.push({ 18630 dependsOn: (data[i] & 0x30) >> 4, 18631 isDependedOn: (data[i] & 0x0c) >> 2, 18632 hasRedundancy: data[i] & 0x03 18633 }); 18634 } 18635 18636 return result; 18637 }, 18638 sidx: parseSidx_1, 18639 smhd: function (data) { 18640 return { 18641 version: data[0], 18642 flags: new Uint8Array(data.subarray(1, 4)), 18643 balance: data[4] + data[5] / 256 18644 }; 18645 }, 18646 stbl: function (data) { 18647 return { 18648 boxes: inspectMp4(data) 18649 }; 18650 }, 18651 ctts: function (data) { 18652 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18653 result = { 18654 version: view.getUint8(0), 18655 flags: new Uint8Array(data.subarray(1, 4)), 18656 compositionOffsets: [] 18657 }, 18658 entryCount = view.getUint32(4), 18659 i; 18660 18661 for (i = 8; entryCount; i += 8, entryCount--) { 18662 result.compositionOffsets.push({ 18663 sampleCount: view.getUint32(i), 18664 sampleOffset: view[result.version === 0 ? 'getUint32' : 'getInt32'](i + 4) 18665 }); 18666 } 18667 18668 return result; 18669 }, 18670 stss: function (data) { 18671 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18672 result = { 18673 version: view.getUint8(0), 18674 flags: new Uint8Array(data.subarray(1, 4)), 18675 syncSamples: [] 18676 }, 18677 entryCount = view.getUint32(4), 18678 i; 18679 18680 for (i = 8; entryCount; i += 4, entryCount--) { 18681 result.syncSamples.push(view.getUint32(i)); 18682 } 18683 18684 return result; 18685 }, 18686 stco: function (data) { 18687 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18688 result = { 18689 version: data[0], 18690 flags: new Uint8Array(data.subarray(1, 4)), 18691 chunkOffsets: [] 18692 }, 18693 entryCount = view.getUint32(4), 18694 i; 18695 18696 for (i = 8; entryCount; i += 4, entryCount--) { 18697 result.chunkOffsets.push(view.getUint32(i)); 18698 } 18699 18700 return result; 18701 }, 18702 stsc: function (data) { 18703 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18704 entryCount = view.getUint32(4), 18705 result = { 18706 version: data[0], 18707 flags: new Uint8Array(data.subarray(1, 4)), 18708 sampleToChunks: [] 18709 }, 18710 i; 18711 18712 for (i = 8; entryCount; i += 12, entryCount--) { 18713 result.sampleToChunks.push({ 18714 firstChunk: view.getUint32(i), 18715 samplesPerChunk: view.getUint32(i + 4), 18716 sampleDescriptionIndex: view.getUint32(i + 8) 18717 }); 18718 } 18719 18720 return result; 18721 }, 18722 stsd: function (data) { 18723 return { 18724 version: data[0], 18725 flags: new Uint8Array(data.subarray(1, 4)), 18726 sampleDescriptions: inspectMp4(data.subarray(8)) 18727 }; 18728 }, 18729 stsz: function (data) { 18730 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18731 result = { 18732 version: data[0], 18733 flags: new Uint8Array(data.subarray(1, 4)), 18734 sampleSize: view.getUint32(4), 18735 entries: [] 18736 }, 18737 i; 18738 18739 for (i = 12; i < data.byteLength; i += 4) { 18740 result.entries.push(view.getUint32(i)); 18741 } 18742 18743 return result; 18744 }, 18745 stts: function (data) { 18746 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18747 result = { 18748 version: data[0], 18749 flags: new Uint8Array(data.subarray(1, 4)), 18750 timeToSamples: [] 18751 }, 18752 entryCount = view.getUint32(4), 18753 i; 18754 18755 for (i = 8; entryCount; i += 8, entryCount--) { 18756 result.timeToSamples.push({ 18757 sampleCount: view.getUint32(i), 18758 sampleDelta: view.getUint32(i + 4) 18759 }); 18760 } 18761 18762 return result; 18763 }, 18764 styp: function (data) { 18765 return parse.ftyp(data); 18766 }, 18767 tfdt: parseTfdt$3, 18768 tfhd: parseTfhd$2, 18769 tkhd: function (data) { 18770 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 18771 i = 4, 18772 result = { 18773 version: view.getUint8(0), 18774 flags: new Uint8Array(data.subarray(1, 4)) 18775 }; 18776 18777 if (result.version === 1) { 18778 i += 4; 18779 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 18780 18781 i += 8; 18782 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 18783 18784 i += 4; 18785 result.trackId = view.getUint32(i); 18786 i += 4; 18787 i += 8; 18788 result.duration = view.getUint32(i); // truncating top 4 bytes 18789 } else { 18790 result.creationTime = parseMp4Date(view.getUint32(i)); 18791 i += 4; 18792 result.modificationTime = parseMp4Date(view.getUint32(i)); 18793 i += 4; 18794 result.trackId = view.getUint32(i); 18795 i += 4; 18796 i += 4; 18797 result.duration = view.getUint32(i); 18798 } 18799 18800 i += 4; 18801 i += 2 * 4; 18802 result.layer = view.getUint16(i); 18803 i += 2; 18804 result.alternateGroup = view.getUint16(i); 18805 i += 2; // convert fixed-point, base 16 back to a number 18806 18807 result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8; 18808 i += 2; 18809 i += 2; 18810 result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4)); 18811 i += 9 * 4; 18812 result.width = view.getUint16(i) + view.getUint16(i + 2) / 65536; 18813 i += 4; 18814 result.height = view.getUint16(i) + view.getUint16(i + 2) / 65536; 18815 return result; 18816 }, 18817 traf: function (data) { 18818 return { 18819 boxes: inspectMp4(data) 18820 }; 18821 }, 18822 trak: function (data) { 18823 return { 18824 boxes: inspectMp4(data) 18825 }; 18826 }, 18827 trex: function (data) { 18828 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 18829 return { 18830 version: data[0], 18831 flags: new Uint8Array(data.subarray(1, 4)), 18832 trackId: view.getUint32(4), 18833 defaultSampleDescriptionIndex: view.getUint32(8), 18834 defaultSampleDuration: view.getUint32(12), 18835 defaultSampleSize: view.getUint32(16), 18836 sampleDependsOn: data[20] & 0x03, 18837 sampleIsDependedOn: (data[21] & 0xc0) >> 6, 18838 sampleHasRedundancy: (data[21] & 0x30) >> 4, 18839 samplePaddingValue: (data[21] & 0x0e) >> 1, 18840 sampleIsDifferenceSample: !!(data[21] & 0x01), 18841 sampleDegradationPriority: view.getUint16(22) 18842 }; 18843 }, 18844 trun: parseTrun$2, 18845 'url ': function (data) { 18846 return { 18847 version: data[0], 18848 flags: new Uint8Array(data.subarray(1, 4)) 18849 }; 18850 }, 18851 vmhd: function (data) { 18852 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 18853 return { 18854 version: data[0], 18855 flags: new Uint8Array(data.subarray(1, 4)), 18856 graphicsmode: view.getUint16(4), 18857 opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)]) 18858 }; 18859 } 18860 }; 18861 /** 18862 * Return a javascript array of box objects parsed from an ISO base 18863 * media file. 18864 * @param data {Uint8Array} the binary data of the media to be inspected 18865 * @return {array} a javascript array of potentially nested box objects 18866 */ 18867 18868 18869 inspectMp4 = function (data) { 18870 var i = 0, 18871 result = [], 18872 view, 18873 size, 18874 type, 18875 end, 18876 box; // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API 18877 18878 var ab = new ArrayBuffer(data.length); 18879 var v = new Uint8Array(ab); 18880 18881 for (var z = 0; z < data.length; ++z) { 18882 v[z] = data[z]; 18883 } 18884 18885 view = new DataView(ab); 18886 18887 while (i < data.byteLength) { 18888 // parse box data 18889 size = view.getUint32(i); 18890 type = parseType$2(data.subarray(i + 4, i + 8)); 18891 end = size > 1 ? i + size : data.byteLength; // parse type-specific data 18892 18893 box = (parse[type] || function (data) { 18894 return { 18895 data: data 18896 }; 18897 })(data.subarray(i + 8, end)); 18898 18899 box.size = size; 18900 box.type = type; // store this box and move to the next 18901 18902 result.push(box); 18903 i = end; 18904 } 18905 18906 return result; 18907 }; 18908 /** 18909 * Returns a textual representation of the javascript represtentation 18910 * of an MP4 file. You can use it as an alternative to 18911 * JSON.stringify() to compare inspected MP4s. 18912 * @param inspectedMp4 {array} the parsed array of boxes in an MP4 18913 * file 18914 * @param depth {number} (optional) the number of ancestor boxes of 18915 * the elements of inspectedMp4. Assumed to be zero if unspecified. 18916 * @return {string} a text representation of the parsed MP4 18917 */ 18918 18919 18920 textifyMp4 = function (inspectedMp4, depth) { 18921 var indent; 18922 depth = depth || 0; 18923 indent = new Array(depth * 2 + 1).join(' '); // iterate over all the boxes 18924 18925 return inspectedMp4.map(function (box, index) { 18926 // list the box type first at the current indentation level 18927 return indent + box.type + '\n' + // the type is already included and handle child boxes separately 18928 Object.keys(box).filter(function (key) { 18929 return key !== 'type' && key !== 'boxes'; // output all the box properties 18930 }).map(function (key) { 18931 var prefix = indent + ' ' + key + ': ', 18932 value = box[key]; // print out raw bytes as hexademical 18933 18934 if (value instanceof Uint8Array || value instanceof Uint32Array) { 18935 var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)).map(function (byte) { 18936 return ' ' + ('00' + byte.toString(16)).slice(-2); 18937 }).join('').match(/.{1,24}/g); 18938 18939 if (!bytes) { 18940 return prefix + '<>'; 18941 } 18942 18943 if (bytes.length === 1) { 18944 return prefix + '<' + bytes.join('').slice(1) + '>'; 18945 } 18946 18947 return prefix + '<\n' + bytes.map(function (line) { 18948 return indent + ' ' + line; 18949 }).join('\n') + '\n' + indent + ' >'; 18950 } // stringify generic objects 18951 18952 18953 return prefix + JSON.stringify(value, null, 2).split('\n').map(function (line, index) { 18954 if (index === 0) { 18955 return line; 18956 } 18957 18958 return indent + ' ' + line; 18959 }).join('\n'); 18960 }).join('\n') + ( // recursively textify the child boxes 18961 box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : ''); 18962 }).join('\n'); 18963 }; 18964 18965 var mp4Inspector = { 18966 inspect: inspectMp4, 18967 textify: textifyMp4, 18968 parseType: parseType$2, 18969 findBox: findBox$4, 18970 parseTraf: parse.traf, 18971 parseTfdt: parse.tfdt, 18972 parseHdlr: parse.hdlr, 18973 parseTfhd: parse.tfhd, 18974 parseTrun: parse.trun, 18975 parseSidx: parse.sidx 18976 }; 18977 /** 18978 * Returns the first string in the data array ending with a null char '\0' 18979 * @param {UInt8} data
vendor: 2,985 bytes, lines 18979-19070
18979 18980 * @returns the string with the null char 18981 */ 18982 18983 var uint8ToCString$1 = function (data) { 18984 var index = 0; 18985 var curChar = String.fromCharCode(data[index]); 18986 var retString = ''; 18987 18988 while (curChar !== '\0') { 18989 retString += curChar; 18990 index++; 18991 curChar = String.fromCharCode(data[index]); 18992 } // Add nullChar 18993 18994 18995 retString += curChar; 18996 return retString; 18997 }; 18998 18999 var string = { 19000 uint8ToCString: uint8ToCString$1 19001 }; 19002 var uint8ToCString = string.uint8ToCString; 19003 var getUint64$1 = numbers.getUint64; 19004 /** 19005 * Based on: ISO/IEC 23009 Section: 5.10.3.3 19006 * References: 19007 * https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format 19008 * https://aomediacodec.github.io/id3-emsg/ 19009 * 19010 * Takes emsg box data as a uint8 array and returns a emsg box object 19011 * @param {UInt8Array} boxData data from emsg box 19012 * @returns A parsed emsg box object 19013 */ 19014 19015 var parseEmsgBox = function (boxData) { 19016 // version + flags 19017 var offset = 4; 19018 var version = boxData[0]; 19019 var scheme_id_uri, value, timescale, presentation_time, presentation_time_delta, event_duration, id, message_data; 19020 19021 if (version === 0) { 19022 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 19023 offset += scheme_id_uri.length; 19024 value = uint8ToCString(boxData.subarray(offset)); 19025 offset += value.length; 19026 var dv = new DataView(boxData.buffer); 19027 timescale = dv.getUint32(offset); 19028 offset += 4; 19029 presentation_time_delta = dv.getUint32(offset); 19030 offset += 4; 19031 event_duration = dv.getUint32(offset); 19032 offset += 4; 19033 id = dv.getUint32(offset); 19034 offset += 4; 19035 } else if (version === 1) { 19036 var dv = new DataView(boxData.buffer); 19037 timescale = dv.getUint32(offset); 19038 offset += 4; 19039 presentation_time = getUint64$1(boxData.subarray(offset)); 19040 offset += 8; 19041 event_duration = dv.getUint32(offset); 19042 offset += 4; 19043 id = dv.getUint32(offset); 19044 offset += 4; 19045 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 19046 offset += scheme_id_uri.length; 19047 value = uint8ToCString(boxData.subarray(offset)); 19048 offset += value.length; 19049 } 19050 19051 message_data = new Uint8Array(boxData.subarray(offset, boxData.byteLength)); 19052 var emsgBox = { 19053 scheme_id_uri, 19054 value, 19055 // if timescale is undefined or 0 set to 1 19056 timescale: timescale ? timescale : 1, 19057 presentation_time, 19058 presentation_time_delta, 19059 event_duration, 19060 id, 19061 message_data 19062 }; 19063 return isValidEmsgBox(version, emsgBox) ? emsgBox : undefined; 19064 }; 19065 /** 19066 * Scales a presentation time or time delta with an offset with a provided timescale 19067 * @param {number} presentationTime 19068 * @param {number} timescale 19069 * @param {number} timeDelta 19070 * @param {number} offset
vendor: 13,887 bytes, lines 19070-19457
19070 19071 * @returns the scaled time as a number 19072 */ 19073 19074 19075 var scaleTime = function (presentationTime, timescale, timeDelta, offset) { 19076 return presentationTime || presentationTime === 0 ? presentationTime / timescale : offset + timeDelta / timescale; 19077 }; 19078 /** 19079 * Checks the emsg box data for validity based on the version 19080 * @param {number} version of the emsg box to validate 19081 * @param {Object} emsg the emsg data to validate 19082 * @returns if the box is valid as a boolean 19083 */ 19084 19085 19086 var isValidEmsgBox = function (version, emsg) { 19087 var hasScheme = emsg.scheme_id_uri !== '\0'; 19088 var isValidV0Box = version === 0 && isDefined(emsg.presentation_time_delta) && hasScheme; 19089 var isValidV1Box = version === 1 && isDefined(emsg.presentation_time) && hasScheme; // Only valid versions of emsg are 0 and 1 19090 19091 return !(version > 1) && isValidV0Box || isValidV1Box; 19092 }; // Utility function to check if an object is defined 19093 19094 19095 var isDefined = function (data) { 19096 return data !== undefined || data !== null; 19097 }; 19098 19099 var emsg$1 = { 19100 parseEmsgBox: parseEmsgBox, 19101 scaleTime: scaleTime 19102 }; 19103 var win; 19104 19105 if (typeof window !== "undefined") { 19106 win = window; 19107 } else if (typeof commonjsGlobal !== "undefined") { 19108 win = commonjsGlobal; 19109 } else if (typeof self !== "undefined") { 19110 win = self; 19111 } else { 19112 win = {}; 19113 } 19114 19115 var window_1 = win; 19116 /** 19117 * mux.js 19118 * 19119 * Copyright (c) Brightcove 19120 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 19121 * 19122 * Utilities to detect basic properties and metadata about MP4s. 19123 */ 19124 19125 var toUnsigned = bin.toUnsigned; 19126 var toHexString = bin.toHexString; 19127 var findBox$3 = findBox_1; 19128 var parseType$1 = parseType_1; 19129 var emsg = emsg$1; 19130 var parseTfhd$1 = parseTfhd$2; 19131 var parseTrun$1 = parseTrun$2; 19132 var parseTfdt$2 = parseTfdt$3; 19133 var getUint64 = numbers.getUint64; 19134 var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks, getTimescaleFromMediaHeader$1, getEmsgID3; 19135 var window$2 = window_1; 19136 var parseId3Frames = parseId3.parseId3Frames; 19137 /** 19138 * Parses an MP4 initialization segment and extracts the timescale 19139 * values for any declared tracks. Timescale values indicate the 19140 * number of clock ticks per second to assume for time-based values 19141 * elsewhere in the MP4. 19142 * 19143 * To determine the start time of an MP4, you need two pieces of 19144 * information: the timescale unit and the earliest base media decode 19145 * time. Multiple timescales can be specified within an MP4 but the 19146 * base media decode time is always expressed in the timescale from 19147 * the media header box for the track: 19148 * ``` 19149 * moov > trak > mdia > mdhd.timescale 19150 * ``` 19151 * @param init {Uint8Array} the bytes of the init segment 19152 * @return {object} a hash of track ids to timescale values or null if 19153 * the init segment is malformed. 19154 */ 19155 19156 timescale = function (init) { 19157 var result = {}, 19158 traks = findBox$3(init, ['moov', 'trak']); // mdhd timescale 19159 19160 return traks.reduce(function (result, trak) { 19161 var tkhd, version, index, id, mdhd; 19162 tkhd = findBox$3(trak, ['tkhd'])[0]; 19163 19164 if (!tkhd) { 19165 return null; 19166 } 19167 19168 version = tkhd[0]; 19169 index = version === 0 ? 12 : 20; 19170 id = toUnsigned(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]); 19171 mdhd = findBox$3(trak, ['mdia', 'mdhd'])[0]; 19172 19173 if (!mdhd) { 19174 return null; 19175 } 19176 19177 version = mdhd[0]; 19178 index = version === 0 ? 12 : 20; 19179 result[id] = toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 19180 return result; 19181 }, result); 19182 }; 19183 /** 19184 * Determine the base media decode start time, in seconds, for an MP4 19185 * fragment. If multiple fragments are specified, the earliest time is 19186 * returned. 19187 * 19188 * The base media decode time can be parsed from track fragment 19189 * metadata: 19190 * ``` 19191 * moof > traf > tfdt.baseMediaDecodeTime 19192 * ``` 19193 * It requires the timescale value from the mdhd to interpret. 19194 * 19195 * @param timescale {object} a hash of track ids to timescale values. 19196 * @return {number} the earliest base media decode start time for the 19197 * fragment, in seconds 19198 */ 19199 19200 19201 startTime = function (timescale, fragment) { 19202 var trafs; // we need info from two childrend of each track fragment box 19203 19204 trafs = findBox$3(fragment, ['moof', 'traf']); // determine the start times for each track 19205 19206 var lowestTime = trafs.reduce(function (acc, traf) { 19207 var tfhd = findBox$3(traf, ['tfhd'])[0]; // get the track id from the tfhd 19208 19209 var id = toUnsigned(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); // assume a 90kHz clock if no timescale was specified 19210 19211 var scale = timescale[id] || 90e3; // get the base media decode time from the tfdt 19212 19213 var tfdt = findBox$3(traf, ['tfdt'])[0]; 19214 var dv = new DataView(tfdt.buffer, tfdt.byteOffset, tfdt.byteLength); 19215 var baseTime; // version 1 is 64 bit 19216 19217 if (tfdt[0] === 1) { 19218 baseTime = getUint64(tfdt.subarray(4, 12)); 19219 } else { 19220 baseTime = dv.getUint32(4); 19221 } // convert base time to seconds if it is a valid number. 19222 19223 19224 let seconds; 19225 19226 if (typeof baseTime === 'bigint') { 19227 seconds = baseTime / window$2.BigInt(scale); 19228 } else if (typeof baseTime === 'number' && !isNaN(baseTime)) { 19229 seconds = baseTime / scale; 19230 } 19231 19232 if (seconds < Number.MAX_SAFE_INTEGER) { 19233 seconds = Number(seconds); 19234 } 19235 19236 if (seconds < acc) { 19237 acc = seconds; 19238 } 19239 19240 return acc; 19241 }, Infinity); 19242 return typeof lowestTime === 'bigint' || isFinite(lowestTime) ? lowestTime : 0; 19243 }; 19244 /** 19245 * Determine the composition start, in seconds, for an MP4 19246 * fragment. 19247 * 19248 * The composition start time of a fragment can be calculated using the base 19249 * media decode time, composition time offset, and timescale, as follows: 19250 * 19251 * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale 19252 * 19253 * All of the aforementioned information is contained within a media fragment's 19254 * `traf` box, except for timescale info, which comes from the initialization 19255 * segment, so a track id (also contained within a `traf`) is also necessary to 19256 * associate it with a timescale 19257 * 19258 * 19259 * @param timescales {object} - a hash of track ids to timescale values. 19260 * @param fragment {Unit8Array} - the bytes of a media segment 19261 * @return {number} the composition start time for the fragment, in seconds 19262 **/ 19263 19264 19265 compositionStartTime = function (timescales, fragment) { 19266 var trafBoxes = findBox$3(fragment, ['moof', 'traf']); 19267 var baseMediaDecodeTime = 0; 19268 var compositionTimeOffset = 0; 19269 var trackId; 19270 19271 if (trafBoxes && trafBoxes.length) { 19272 // The spec states that track run samples contained within a `traf` box are contiguous, but 19273 // it does not explicitly state whether the `traf` boxes themselves are contiguous. 19274 // We will assume that they are, so we only need the first to calculate start time. 19275 var tfhd = findBox$3(trafBoxes[0], ['tfhd'])[0]; 19276 var trun = findBox$3(trafBoxes[0], ['trun'])[0]; 19277 var tfdt = findBox$3(trafBoxes[0], ['tfdt'])[0]; 19278 19279 if (tfhd) { 19280 var parsedTfhd = parseTfhd$1(tfhd); 19281 trackId = parsedTfhd.trackId; 19282 } 19283 19284 if (tfdt) { 19285 var parsedTfdt = parseTfdt$2(tfdt); 19286 baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime; 19287 } 19288 19289 if (trun) { 19290 var parsedTrun = parseTrun$1(trun); 19291 19292 if (parsedTrun.samples && parsedTrun.samples.length) { 19293 compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0; 19294 } 19295 } 19296 } // Get timescale for this specific track. Assume a 90kHz clock if no timescale was 19297 // specified. 19298 19299 19300 var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds 19301 19302 if (typeof baseMediaDecodeTime === 'bigint') { 19303 compositionTimeOffset = window$2.BigInt(compositionTimeOffset); 19304 timescale = window$2.BigInt(timescale); 19305 } 19306 19307 var result = (baseMediaDecodeTime + compositionTimeOffset) / timescale; 19308 19309 if (typeof result === 'bigint' && result < Number.MAX_SAFE_INTEGER) { 19310 result = Number(result); 19311 } 19312 19313 return result; 19314 }; 19315 /** 19316 * Find the trackIds of the video tracks in this source. 19317 * Found by parsing the Handler Reference and Track Header Boxes: 19318 * moov > trak > mdia > hdlr 19319 * moov > trak > tkhd 19320 * 19321 * @param {Uint8Array} init - The bytes of the init segment for this source 19322 * @return {Number[]} A list of trackIds 19323 * 19324 * @see ISO-BMFF-12/2015, Section 8.4.3 19325 **/ 19326 19327 19328 getVideoTrackIds = function (init) { 19329 var traks = findBox$3(init, ['moov', 'trak']); 19330 var videoTrackIds = []; 19331 traks.forEach(function (trak) { 19332 var hdlrs = findBox$3(trak, ['mdia', 'hdlr']); 19333 var tkhds = findBox$3(trak, ['tkhd']); 19334 hdlrs.forEach(function (hdlr, index) { 19335 var handlerType = parseType$1(hdlr.subarray(8, 12)); 19336 var tkhd = tkhds[index]; 19337 var view; 19338 var version; 19339 var trackId; 19340 19341 if (handlerType === 'vide') { 19342 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 19343 version = view.getUint8(0); 19344 trackId = version === 0 ? view.getUint32(12) : view.getUint32(20); 19345 videoTrackIds.push(trackId); 19346 } 19347 }); 19348 }); 19349 return videoTrackIds; 19350 }; 19351 19352 getTimescaleFromMediaHeader$1 = function (mdhd) { 19353 // mdhd is a FullBox, meaning it will have its own version as the first byte 19354 var version = mdhd[0]; 19355 var index = version === 0 ? 12 : 20; 19356 return toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 19357 }; 19358 /** 19359 * Get all the video, audio, and hint tracks from a non fragmented 19360 * mp4 segment 19361 */ 19362 19363 19364 getTracks = function (init) { 19365 var traks = findBox$3(init, ['moov', 'trak']); 19366 var tracks = []; 19367 traks.forEach(function (trak) { 19368 var track = {}; 19369 var tkhd = findBox$3(trak, ['tkhd'])[0]; 19370 var view, tkhdVersion; // id 19371 19372 if (tkhd) { 19373 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 19374 tkhdVersion = view.getUint8(0); 19375 track.id = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20); 19376 } 19377 19378 var hdlr = findBox$3(trak, ['mdia', 'hdlr'])[0]; // type 19379 19380 if (hdlr) { 19381 var type = parseType$1(hdlr.subarray(8, 12)); 19382 19383 if (type === 'vide') { 19384 track.type = 'video'; 19385 } else if (type === 'soun') { 19386 track.type = 'audio'; 19387 } else { 19388 track.type = type; 19389 } 19390 } // codec 19391 19392 19393 var stsd = findBox$3(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 19394 19395 if (stsd) { 19396 var sampleDescriptions = stsd.subarray(8); // gives the codec type string 19397 19398 track.codec = parseType$1(sampleDescriptions.subarray(4, 8)); 19399 var codecBox = findBox$3(sampleDescriptions, [track.codec])[0]; 19400 var codecConfig, codecConfigType; 19401 19402 if (codecBox) { 19403 // https://tools.ietf.org/html/rfc6381#section-3.3 19404 if (/^[asm]vc[1-9]$/i.test(track.codec)) { 19405 // we don't need anything but the "config" parameter of the 19406 // avc1 codecBox 19407 codecConfig = codecBox.subarray(78); 19408 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 19409 19410 if (codecConfigType === 'avcC' && codecConfig.length > 11) { 19411 track.codec += '.'; // left padded with zeroes for single digit hex 19412 // profile idc 19413 19414 track.codec += toHexString(codecConfig[9]); // the byte containing the constraint_set flags 19415 19416 track.codec += toHexString(codecConfig[10]); // level idc 19417 19418 track.codec += toHexString(codecConfig[11]); 19419 } else { 19420 // TODO: show a warning that we couldn't parse the codec 19421 // and are using the default 19422 track.codec = 'avc1.4d400d'; 19423 } 19424 } else if (/^mp4[a,v]$/i.test(track.codec)) { 19425 // we do not need anything but the streamDescriptor of the mp4a codecBox 19426 codecConfig = codecBox.subarray(28); 19427 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 19428 19429 if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) { 19430 track.codec += '.' + toHexString(codecConfig[19]); // this value is only a single digit 19431 19432 track.codec += '.' + toHexString(codecConfig[20] >>> 2 & 0x3f).replace(/^0/, ''); 19433 } else { 19434 // TODO: show a warning that we couldn't parse the codec 19435 // and are using the default 19436 track.codec = 'mp4a.40.2'; 19437 } 19438 } else { 19439 // flac, opus, etc 19440 track.codec = track.codec.toLowerCase(); 19441 } 19442 } 19443 } 19444 19445 var mdhd = findBox$3(trak, ['mdia', 'mdhd'])[0]; 19446 19447 if (mdhd) { 19448 track.timescale = getTimescaleFromMediaHeader$1(mdhd); 19449 } 19450 19451 tracks.push(track); 19452 }); 19453 return tracks; 19454 }; 19455 /** 19456 * Returns an array of emsg ID3 data from the provided segmentData. 19457 * An offset can also be provided as the Latest Arrival Time to calculate
19457 19458 * the Event Start Time of v0 EMSG boxes. 19459 * See: https://dashif-documents.azurewebsites.net/Events/master/event.html#Inband-event-timing 19460 * 19461 * @param {Uint8Array} segmentData the segment byte array. 19462 * @param {number} offset the segment start time or Latest Arrival Time,
vendor: 61,727 bytes, lines 19462-21366
19462 19463 * @return {Object[]} an array of ID3 parsed from EMSG boxes 19464 */ 19465 19466 19467 getEmsgID3 = function (segmentData, offset = 0) { 19468 var emsgBoxes = findBox$3(segmentData, ['emsg']); 19469 return emsgBoxes.map(data => { 19470 var parsedBox = emsg.parseEmsgBox(new Uint8Array(data)); 19471 var parsedId3Frames = parseId3Frames(parsedBox.message_data); 19472 return { 19473 cueTime: emsg.scaleTime(parsedBox.presentation_time, parsedBox.timescale, parsedBox.presentation_time_delta, offset), 19474 duration: emsg.scaleTime(parsedBox.event_duration, parsedBox.timescale), 19475 frames: parsedId3Frames 19476 }; 19477 }); 19478 }; 19479 19480 var probe$2 = { 19481 // export mp4 inspector's findBox and parseType for backwards compatibility 19482 findBox: findBox$3, 19483 parseType: parseType$1, 19484 timescale: timescale, 19485 startTime: startTime, 19486 compositionStartTime: compositionStartTime, 19487 videoTrackIds: getVideoTrackIds, 19488 tracks: getTracks, 19489 getTimescaleFromMediaHeader: getTimescaleFromMediaHeader$1, 19490 getEmsgID3: getEmsgID3 19491 }; 19492 const { 19493 parseTrun 19494 } = mp4Inspector; 19495 const { 19496 findBox: findBox$2 19497 } = probe$2; 19498 var window$1 = window_1; 19499 /** 19500 * Utility function for parsing data from mdat boxes. 19501 * @param {Array<Uint8Array>} segment the segment data to create mdat/traf pairs from. 19502 * @returns mdat and traf boxes paired up for easier parsing. 19503 */ 19504 19505 var getMdatTrafPairs$2 = function (segment) { 19506 var trafs = findBox$2(segment, ['moof', 'traf']); 19507 var mdats = findBox$2(segment, ['mdat']); 19508 var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs 19509 19510 mdats.forEach(function (mdat, index) { 19511 var matchingTraf = trafs[index]; 19512 mdatTrafPairs.push({ 19513 mdat: mdat, 19514 traf: matchingTraf 19515 }); 19516 }); 19517 return mdatTrafPairs; 19518 }; 19519 /** 19520 * Parses sample information out of Track Run Boxes and calculates 19521 * the absolute presentation and decode timestamps of each sample. 19522 * 19523 * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed 19524 * @param {Number|BigInt} baseMediaDecodeTime - base media decode time from tfdt 19525 @see ISO-BMFF-12/2015, Section 8.8.12 19526 * @param {Object} tfhd - The parsed Track Fragment Header 19527 * @see inspect.parseTfhd 19528 * @return {Object[]} the parsed samples 19529 * 19530 * @see ISO-BMFF-12/2015, Section 8.8.8 19531 **/ 19532 19533 19534 var parseSamples$2 = function (truns, baseMediaDecodeTime, tfhd) { 19535 var currentDts = baseMediaDecodeTime; 19536 var defaultSampleDuration = tfhd.defaultSampleDuration || 0; 19537 var defaultSampleSize = tfhd.defaultSampleSize || 0; 19538 var trackId = tfhd.trackId; 19539 var allSamples = []; 19540 truns.forEach(function (trun) { 19541 // Note: We currently do not parse the sample table as well 19542 // as the trun. It's possible some sources will require this. 19543 // moov > trak > mdia > minf > stbl 19544 var trackRun = parseTrun(trun); 19545 var samples = trackRun.samples; 19546 samples.forEach(function (sample) { 19547 if (sample.duration === undefined) { 19548 sample.duration = defaultSampleDuration; 19549 } 19550 19551 if (sample.size === undefined) { 19552 sample.size = defaultSampleSize; 19553 } 19554 19555 sample.trackId = trackId; 19556 sample.dts = currentDts; 19557 19558 if (sample.compositionTimeOffset === undefined) { 19559 sample.compositionTimeOffset = 0; 19560 } 19561 19562 if (typeof currentDts === 'bigint') { 19563 sample.pts = currentDts + window$1.BigInt(sample.compositionTimeOffset); 19564 currentDts += window$1.BigInt(sample.duration); 19565 } else { 19566 sample.pts = currentDts + sample.compositionTimeOffset; 19567 currentDts += sample.duration; 19568 } 19569 }); 19570 allSamples = allSamples.concat(samples); 19571 }); 19572 return allSamples; 19573 }; 19574 19575 var samples = { 19576 getMdatTrafPairs: getMdatTrafPairs$2, 19577 parseSamples: parseSamples$2 19578 }; 19579 /** 19580 * mux.js 19581 * 19582 * Copyright (c) Brightcove 19583 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 19584 * 19585 * Reads in-band CEA-708 captions out of FMP4 segments. 19586 * @see https://en.wikipedia.org/wiki/CEA-708 19587 */ 19588 19589 var discardEmulationPreventionBytes = captionPacketParser.discardEmulationPreventionBytes; 19590 var CaptionStream = captionStream.CaptionStream; 19591 var findBox$1 = findBox_1; 19592 var parseTfdt$1 = parseTfdt$3; 19593 var parseTfhd = parseTfhd$2; 19594 var { 19595 getMdatTrafPairs: getMdatTrafPairs$1, 19596 parseSamples: parseSamples$1 19597 } = samples; 19598 /** 19599 * Maps an offset in the mdat to a sample based on the the size of the samples. 19600 * Assumes that `parseSamples` has been called first. 19601 * 19602 * @param {Number} offset - The offset into the mdat 19603 * @param {Object[]} samples - An array of samples, parsed using `parseSamples` 19604 * @return {?Object} The matching sample, or null if no match was found. 19605 * 19606 * @see ISO-BMFF-12/2015, Section 8.8.8 19607 **/ 19608 19609 var mapToSample = function (offset, samples) { 19610 var approximateOffset = offset; 19611 19612 for (var i = 0; i < samples.length; i++) { 19613 var sample = samples[i]; 19614 19615 if (approximateOffset < sample.size) { 19616 return sample; 19617 } 19618 19619 approximateOffset -= sample.size; 19620 } 19621 19622 return null; 19623 }; 19624 /** 19625 * Finds SEI nal units contained in a Media Data Box. 19626 * Assumes that `parseSamples` has been called first. 19627 * 19628 * @param {Uint8Array} avcStream - The bytes of the mdat 19629 * @param {Object[]} samples - The samples parsed out by `parseSamples` 19630 * @param {Number} trackId - The trackId of this video track 19631 * @return {Object[]} seiNals - the parsed SEI NALUs found. 19632 * The contents of the seiNal should match what is expected by 19633 * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts) 19634 * 19635 * @see ISO-BMFF-12/2015, Section 8.1.1 19636 * @see Rec. ITU-T H.264, 7.3.2.3.1 19637 **/ 19638 19639 19640 var findSeiNals = function (avcStream, samples, trackId) { 19641 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 19642 result = { 19643 logs: [], 19644 seiNals: [] 19645 }, 19646 seiNal, 19647 i, 19648 length, 19649 lastMatchedSample; 19650 19651 for (i = 0; i + 4 < avcStream.length; i += length) { 19652 length = avcView.getUint32(i); 19653 i += 4; // Bail if this doesn't appear to be an H264 stream 19654 19655 if (length <= 0) { 19656 continue; 19657 } 19658 19659 switch (avcStream[i] & 0x1F) { 19660 case 0x06: 19661 var data = avcStream.subarray(i + 1, i + 1 + length); 19662 var matchingSample = mapToSample(i, samples); 19663 seiNal = { 19664 nalUnitType: 'sei_rbsp', 19665 size: length, 19666 data: data, 19667 escapedRBSP: discardEmulationPreventionBytes(data), 19668 trackId: trackId 19669 }; 19670 19671 if (matchingSample) { 19672 seiNal.pts = matchingSample.pts; 19673 seiNal.dts = matchingSample.dts; 19674 lastMatchedSample = matchingSample; 19675 } else if (lastMatchedSample) { 19676 // If a matching sample cannot be found, use the last 19677 // sample's values as they should be as close as possible 19678 seiNal.pts = lastMatchedSample.pts; 19679 seiNal.dts = lastMatchedSample.dts; 19680 } else { 19681 result.logs.push({ 19682 level: 'warn', 19683 message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.' 19684 }); 19685 break; 19686 } 19687 19688 result.seiNals.push(seiNal); 19689 break; 19690 } 19691 } 19692 19693 return result; 19694 }; 19695 /** 19696 * Parses out caption nals from an FMP4 segment's video tracks. 19697 * 19698 * @param {Uint8Array} segment - The bytes of a single segment 19699 * @param {Number} videoTrackId - The trackId of a video track in the segment 19700 * @return {Object.<Number, Object[]>} A mapping of video trackId to 19701 * a list of seiNals found in that track 19702 **/ 19703 19704 19705 var parseCaptionNals = function (segment, videoTrackId) { 19706 var captionNals = {}; 19707 var mdatTrafPairs = getMdatTrafPairs$1(segment); 19708 mdatTrafPairs.forEach(function (pair) { 19709 var mdat = pair.mdat; 19710 var traf = pair.traf; 19711 var tfhd = findBox$1(traf, ['tfhd']); // Exactly 1 tfhd per traf 19712 19713 var headerInfo = parseTfhd(tfhd[0]); 19714 var trackId = headerInfo.trackId; 19715 var tfdt = findBox$1(traf, ['tfdt']); // Either 0 or 1 tfdt per traf 19716 19717 var baseMediaDecodeTime = tfdt.length > 0 ? parseTfdt$1(tfdt[0]).baseMediaDecodeTime : 0; 19718 var truns = findBox$1(traf, ['trun']); 19719 var samples; 19720 var result; // Only parse video data for the chosen video track 19721 19722 if (videoTrackId === trackId && truns.length > 0) { 19723 samples = parseSamples$1(truns, baseMediaDecodeTime, headerInfo); 19724 result = findSeiNals(mdat, samples, trackId); 19725 19726 if (!captionNals[trackId]) { 19727 captionNals[trackId] = { 19728 seiNals: [], 19729 logs: [] 19730 }; 19731 } 19732 19733 captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals); 19734 captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs); 19735 } 19736 }); 19737 return captionNals; 19738 }; 19739 /** 19740 * Parses out inband captions from an MP4 container and returns 19741 * caption objects that can be used by WebVTT and the TextTrack API. 19742 * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue 19743 * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack 19744 * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first 19745 * 19746 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 19747 * @param {Number} trackId - The id of the video track to parse 19748 * @param {Number} timescale - The timescale for the video track from the init segment 19749 * 19750 * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks 19751 * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds 19752 * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds 19753 * @return {Object[]} parsedCaptions[].content - A list of individual caption segments 19754 * @return {String} parsedCaptions[].content.text - The visible content of the caption segment 19755 * @return {Number} parsedCaptions[].content.line - The line height from 1-15 for positioning of the caption segment 19756 * @return {Number} parsedCaptions[].content.position - The column indent percentage for cue positioning from 10-80 19757 **/ 19758 19759 19760 var parseEmbeddedCaptions = function (segment, trackId, timescale) { 19761 var captionNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 19762 19763 if (trackId === null) { 19764 return null; 19765 } 19766 19767 captionNals = parseCaptionNals(segment, trackId); 19768 var trackNals = captionNals[trackId] || {}; 19769 return { 19770 seiNals: trackNals.seiNals, 19771 logs: trackNals.logs, 19772 timescale: timescale 19773 }; 19774 }; 19775 /** 19776 * Converts SEI NALUs into captions that can be used by video.js 19777 **/ 19778 19779 19780 var CaptionParser = function () { 19781 var isInitialized = false; 19782 var captionStream; // Stores segments seen before trackId and timescale are set 19783 19784 var segmentCache; // Stores video track ID of the track being parsed 19785 19786 var trackId; // Stores the timescale of the track being parsed 19787 19788 var timescale; // Stores captions parsed so far 19789 19790 var parsedCaptions; // Stores whether we are receiving partial data or not 19791 19792 var parsingPartial; 19793 /** 19794 * A method to indicate whether a CaptionParser has been initalized 19795 * @returns {Boolean} 19796 **/ 19797 19798 this.isInitialized = function () { 19799 return isInitialized; 19800 }; 19801 /** 19802 * Initializes the underlying CaptionStream, SEI NAL parsing 19803 * and management, and caption collection 19804 **/ 19805 19806 19807 this.init = function (options) { 19808 captionStream = new CaptionStream(); 19809 isInitialized = true; 19810 parsingPartial = options ? options.isPartial : false; // Collect dispatched captions 19811 19812 captionStream.on('data', function (event) { 19813 // Convert to seconds in the source's timescale 19814 event.startTime = event.startPts / timescale; 19815 event.endTime = event.endPts / timescale; 19816 parsedCaptions.captions.push(event); 19817 parsedCaptions.captionStreams[event.stream] = true; 19818 }); 19819 captionStream.on('log', function (log) { 19820 parsedCaptions.logs.push(log); 19821 }); 19822 }; 19823 /** 19824 * Determines if a new video track will be selected 19825 * or if the timescale changed 19826 * @return {Boolean} 19827 **/ 19828 19829 19830 this.isNewInit = function (videoTrackIds, timescales) { 19831 if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) { 19832 return false; 19833 } 19834 19835 return trackId !== videoTrackIds[0] || timescale !== timescales[trackId]; 19836 }; 19837 /** 19838 * Parses out SEI captions and interacts with underlying 19839 * CaptionStream to return dispatched captions 19840 * 19841 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 19842 * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment 19843 * @param {Object.<Number, Number>} timescales - The timescales found in the init segment 19844 * @see parseEmbeddedCaptions 19845 * @see m2ts/caption-stream.js 19846 **/ 19847 19848 19849 this.parse = function (segment, videoTrackIds, timescales) { 19850 var parsedData; 19851 19852 if (!this.isInitialized()) { 19853 return null; // This is not likely to be a video segment 19854 } else if (!videoTrackIds || !timescales) { 19855 return null; 19856 } else if (this.isNewInit(videoTrackIds, timescales)) { 19857 // Use the first video track only as there is no 19858 // mechanism to switch to other video tracks 19859 trackId = videoTrackIds[0]; 19860 timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment 19861 // data until we have one. 19862 // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 19863 } else if (trackId === null || !timescale) { 19864 segmentCache.push(segment); 19865 return null; 19866 } // Now that a timescale and trackId is set, parse cached segments 19867 19868 19869 while (segmentCache.length > 0) { 19870 var cachedSegment = segmentCache.shift(); 19871 this.parse(cachedSegment, videoTrackIds, timescales); 19872 } 19873 19874 parsedData = parseEmbeddedCaptions(segment, trackId, timescale); 19875 19876 if (parsedData && parsedData.logs) { 19877 parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs); 19878 } 19879 19880 if (parsedData === null || !parsedData.seiNals) { 19881 if (parsedCaptions.logs.length) { 19882 return { 19883 logs: parsedCaptions.logs, 19884 captions: [], 19885 captionStreams: [] 19886 }; 19887 } 19888 19889 return null; 19890 } 19891 19892 this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched 19893 19894 this.flushStream(); 19895 return parsedCaptions; 19896 }; 19897 /** 19898 * Pushes SEI NALUs onto CaptionStream 19899 * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals` 19900 * Assumes that `parseCaptionNals` has been called first 19901 * @see m2ts/caption-stream.js 19902 **/ 19903 19904 19905 this.pushNals = function (nals) { 19906 if (!this.isInitialized() || !nals || nals.length === 0) { 19907 return null; 19908 } 19909 19910 nals.forEach(function (nal) { 19911 captionStream.push(nal); 19912 }); 19913 }; 19914 /** 19915 * Flushes underlying CaptionStream to dispatch processed, displayable captions 19916 * @see m2ts/caption-stream.js 19917 **/ 19918 19919 19920 this.flushStream = function () { 19921 if (!this.isInitialized()) { 19922 return null; 19923 } 19924 19925 if (!parsingPartial) { 19926 captionStream.flush(); 19927 } else { 19928 captionStream.partialFlush(); 19929 } 19930 }; 19931 /** 19932 * Reset caption buckets for new data 19933 **/ 19934 19935 19936 this.clearParsedCaptions = function () { 19937 parsedCaptions.captions = []; 19938 parsedCaptions.captionStreams = {}; 19939 parsedCaptions.logs = []; 19940 }; 19941 /** 19942 * Resets underlying CaptionStream 19943 * @see m2ts/caption-stream.js 19944 **/ 19945 19946 19947 this.resetCaptionStream = function () { 19948 if (!this.isInitialized()) { 19949 return null; 19950 } 19951 19952 captionStream.reset(); 19953 }; 19954 /** 19955 * Convenience method to clear all captions flushed from the 19956 * CaptionStream and still being parsed 19957 * @see m2ts/caption-stream.js 19958 **/ 19959 19960 19961 this.clearAllCaptions = function () { 19962 this.clearParsedCaptions(); 19963 this.resetCaptionStream(); 19964 }; 19965 /** 19966 * Reset caption parser 19967 **/ 19968 19969 19970 this.reset = function () { 19971 segmentCache = []; 19972 trackId = null; 19973 timescale = null; 19974 19975 if (!parsedCaptions) { 19976 parsedCaptions = { 19977 captions: [], 19978 // CC1, CC2, CC3, CC4 19979 captionStreams: {}, 19980 logs: [] 19981 }; 19982 } else { 19983 this.clearParsedCaptions(); 19984 } 19985 19986 this.resetCaptionStream(); 19987 }; 19988 19989 this.reset(); 19990 }; 19991 19992 var captionParser = CaptionParser; 19993 const { 19994 parseTfdt 19995 } = mp4Inspector; 19996 const findBox = findBox_1; 19997 const { 19998 getTimescaleFromMediaHeader 19999 } = probe$2; 20000 const { 20001 parseSamples, 20002 getMdatTrafPairs 20003 } = samples; 20004 /** 20005 * Module for parsing WebVTT text and styles from FMP4 segments. 20006 * Based on the ISO/IEC 14496-30. 20007 */ 20008 20009 const WebVttParser = function () { 20010 // default timescale to 90k 20011 let timescale = 90e3; 20012 /** 20013 * Parses the timescale from the init segment. 20014 * @param {Array<Uint8Array>} segment The initialization segment to parse the timescale from. 20015 */ 20016 20017 this.init = function (segment) { 20018 // We just need the timescale from the init segment. 20019 const mdhd = findBox(segment, ['moov', 'trak', 'mdia', 'mdhd'])[0]; 20020 20021 if (mdhd) { 20022 timescale = getTimescaleFromMediaHeader(mdhd); 20023 } 20024 }; 20025 /** 20026 * Parses a WebVTT FMP4 segment. 20027 * @param {Array<Uint8Array>} segment The content segment to parse the WebVTT cues from. 20028 * @returns The WebVTT cue text, styling, and timing info as an array of cue objects. 20029 */ 20030 20031 20032 this.parseSegment = function (segment) { 20033 const vttCues = []; 20034 const mdatTrafPairs = getMdatTrafPairs(segment); 20035 let baseMediaDecodeTime = 0; 20036 mdatTrafPairs.forEach(function (pair) { 20037 const mdatBox = pair.mdat; 20038 const trafBox = pair.traf; // zero or one. 20039 20040 const tfdtBox = findBox(trafBox, ['tfdt'])[0]; // zero or one. 20041 20042 const tfhdBox = findBox(trafBox, ['tfhd'])[0]; // zero or more. 20043 20044 const trunBoxes = findBox(trafBox, ['trun']); 20045 20046 if (tfdtBox) { 20047 const tfdt = parseTfdt(tfdtBox); 20048 baseMediaDecodeTime = tfdt.baseMediaDecodeTime; 20049 } 20050 20051 if (trunBoxes.length && tfhdBox) { 20052 const samples = parseSamples(trunBoxes, baseMediaDecodeTime, tfhdBox); 20053 let mdatOffset = 0; 20054 samples.forEach(function (sample) { 20055 // decode utf8 payload 20056 const UTF_8 = 'utf-8'; 20057 const textDecoder = new TextDecoder(UTF_8); // extract sample data from the mdat box. 20058 // WebVTT Sample format: 20059 // Exactly one VTTEmptyCueBox box 20060 // OR one or more VTTCueBox boxes. 20061 20062 const sampleData = mdatBox.slice(mdatOffset, mdatOffset + sample.size); // single vtte box. 20063 20064 const vtteBox = findBox(sampleData, ['vtte'])[0]; // empty box 20065 20066 if (vtteBox) { 20067 mdatOffset += sample.size; 20068 return; 20069 } // TODO: Support 'vtta' boxes. 20070 // VTTAdditionalTextBoxes can be interleaved between VTTCueBoxes. 20071 20072 20073 const vttcBoxes = findBox(sampleData, ['vttc']); 20074 vttcBoxes.forEach(function (vttcBox) { 20075 // mandatory payload box. 20076 const paylBox = findBox(vttcBox, ['payl'])[0]; // optional settings box 20077 20078 const sttgBox = findBox(vttcBox, ['sttg'])[0]; 20079 const start = sample.pts / timescale; 20080 const end = (sample.pts + sample.duration) / timescale; 20081 let cueText, settings; // contains cue text. 20082 20083 if (paylBox) { 20084 try { 20085 cueText = textDecoder.decode(paylBox); 20086 } catch (e) { 20087 console.error(e); 20088 } 20089 } // settings box contains styling. 20090 20091 20092 if (sttgBox) { 20093 try { 20094 settings = textDecoder.decode(sttgBox); 20095 } catch (e) { 20096 console.error(e); 20097 } 20098 } 20099 20100 if (sample.duration && cueText) { 20101 vttCues.push({ 20102 cueText, 20103 start, 20104 end, 20105 settings 20106 }); 20107 } 20108 }); 20109 mdatOffset += sample.size; 20110 }); 20111 } 20112 }); 20113 return vttCues; 20114 }; 20115 }; 20116 20117 var webvttParser = WebVttParser; 20118 /** 20119 * mux.js 20120 * 20121 * Copyright (c) Brightcove 20122 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 20123 * 20124 * Utilities to detect basic properties and metadata about TS Segments. 20125 */ 20126 20127 var StreamTypes$1 = streamTypes; 20128 20129 var parsePid = function (packet) { 20130 var pid = packet[1] & 0x1f; 20131 pid <<= 8; 20132 pid |= packet[2]; 20133 return pid; 20134 }; 20135 20136 var parsePayloadUnitStartIndicator = function (packet) { 20137 return !!(packet[1] & 0x40); 20138 }; 20139 20140 var parseAdaptionField = function (packet) { 20141 var offset = 0; // if an adaption field is present, its length is specified by the 20142 // fifth byte of the TS packet header. The adaptation field is 20143 // used to add stuffing to PES packets that don't fill a complete 20144 // TS packet, and to specify some forms of timing and control data 20145 // that we do not currently use. 20146 20147 if ((packet[3] & 0x30) >>> 4 > 0x01) { 20148 offset += packet[4] + 1; 20149 } 20150 20151 return offset; 20152 }; 20153 20154 var parseType = function (packet, pmtPid) { 20155 var pid = parsePid(packet); 20156 20157 if (pid === 0) { 20158 return 'pat'; 20159 } else if (pid === pmtPid) { 20160 return 'pmt'; 20161 } else if (pmtPid) { 20162 return 'pes'; 20163 } 20164 20165 return null; 20166 }; 20167 20168 var parsePat = function (packet) { 20169 var pusi = parsePayloadUnitStartIndicator(packet); 20170 var offset = 4 + parseAdaptionField(packet); 20171 20172 if (pusi) { 20173 offset += packet[offset] + 1; 20174 } 20175 20176 return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; 20177 }; 20178 20179 var parsePmt = function (packet) { 20180 var programMapTable = {}; 20181 var pusi = parsePayloadUnitStartIndicator(packet); 20182 var payloadOffset = 4 + parseAdaptionField(packet); 20183 20184 if (pusi) { 20185 payloadOffset += packet[payloadOffset] + 1; 20186 } // PMTs can be sent ahead of the time when they should actually 20187 // take effect. We don't believe this should ever be the case 20188 // for HLS but we'll ignore "forward" PMT declarations if we see 20189 // them. Future PMT declarations have the current_next_indicator 20190 // set to zero. 20191 20192 20193 if (!(packet[payloadOffset + 5] & 0x01)) { 20194 return; 20195 } 20196 20197 var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section 20198 20199 sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; 20200 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 20201 // long the program info descriptors are 20202 20203 programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table 20204 20205 var offset = 12 + programInfoLength; 20206 20207 while (offset < tableEnd) { 20208 var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type 20209 20210 programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; // move to the next table entry 20211 // skip past the elementary stream descriptors, if present 20212 20213 offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; 20214 } 20215 20216 return programMapTable; 20217 }; 20218 20219 var parsePesType = function (packet, programMapTable) { 20220 var pid = parsePid(packet); 20221 var type = programMapTable[pid]; 20222 20223 switch (type) { 20224 case StreamTypes$1.H264_STREAM_TYPE: 20225 return 'video'; 20226 20227 case StreamTypes$1.ADTS_STREAM_TYPE: 20228 return 'audio'; 20229 20230 case StreamTypes$1.METADATA_STREAM_TYPE: 20231 return 'timed-metadata'; 20232 20233 default: 20234 return null; 20235 } 20236 }; 20237 20238 var parsePesTime = function (packet) { 20239 var pusi = parsePayloadUnitStartIndicator(packet); 20240 20241 if (!pusi) { 20242 return null; 20243 } 20244 20245 var offset = 4 + parseAdaptionField(packet); 20246 20247 if (offset >= packet.byteLength) { 20248 // From the H 222.0 MPEG-TS spec 20249 // "For transport stream packets carrying PES packets, stuffing is needed when there 20250 // is insufficient PES packet data to completely fill the transport stream packet 20251 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 20252 // the sum of the lengths of the data elements in it, so that the payload bytes 20253 // remaining after the adaptation field exactly accommodates the available PES packet 20254 // data." 20255 // 20256 // If the offset is >= the length of the packet, then the packet contains no data 20257 // and instead is just adaption field stuffing bytes 20258 return null; 20259 } 20260 20261 var pes = null; 20262 var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value 20263 // and a DTS value. Determine what combination of values is 20264 // available to work with. 20265 20266 ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 20267 // performs all bitwise operations on 32-bit integers but javascript 20268 // supports a much greater range (52-bits) of integer using standard 20269 // mathematical operations. 20270 // We construct a 31-bit value using bitwise operators over the 31 20271 // most significant bits and then multiply by 4 (equal to a left-shift 20272 // of 2) before we add the final 2 least significant bits of the 20273 // timestamp (equal to an OR.) 20274 20275 if (ptsDtsFlags & 0xC0) { 20276 pes = {}; // the PTS and DTS are not written out directly. For information 20277 // on how they are encoded, see 20278 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 20279 20280 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; 20281 pes.pts *= 4; // Left shift by 2 20282 20283 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 20284 20285 pes.dts = pes.pts; 20286 20287 if (ptsDtsFlags & 0x40) { 20288 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; 20289 pes.dts *= 4; // Left shift by 2 20290 20291 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 20292 } 20293 } 20294 20295 return pes; 20296 }; 20297 20298 var parseNalUnitType = function (type) { 20299 switch (type) { 20300 case 0x05: 20301 return 'slice_layer_without_partitioning_rbsp_idr'; 20302 20303 case 0x06: 20304 return 'sei_rbsp'; 20305 20306 case 0x07: 20307 return 'seq_parameter_set_rbsp'; 20308 20309 case 0x08: 20310 return 'pic_parameter_set_rbsp'; 20311 20312 case 0x09: 20313 return 'access_unit_delimiter_rbsp'; 20314 20315 default: 20316 return null; 20317 } 20318 }; 20319 20320 var videoPacketContainsKeyFrame = function (packet) { 20321 var offset = 4 + parseAdaptionField(packet); 20322 var frameBuffer = packet.subarray(offset); 20323 var frameI = 0; 20324 var frameSyncPoint = 0; 20325 var foundKeyFrame = false; 20326 var nalType; // advance the sync point to a NAL start, if necessary 20327 20328 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 20329 if (frameBuffer[frameSyncPoint + 2] === 1) { 20330 // the sync point is properly aligned 20331 frameI = frameSyncPoint + 5; 20332 break; 20333 } 20334 } 20335 20336 while (frameI < frameBuffer.byteLength) { 20337 // look at the current byte to determine if we've hit the end of 20338 // a NAL unit boundary 20339 switch (frameBuffer[frameI]) { 20340 case 0: 20341 // skip past non-sync sequences 20342 if (frameBuffer[frameI - 1] !== 0) { 20343 frameI += 2; 20344 break; 20345 } else if (frameBuffer[frameI - 2] !== 0) { 20346 frameI++; 20347 break; 20348 } 20349 20350 if (frameSyncPoint + 3 !== frameI - 2) { 20351 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 20352 20353 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 20354 foundKeyFrame = true; 20355 } 20356 } // drop trailing zeroes 20357 20358 20359 do { 20360 frameI++; 20361 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 20362 20363 frameSyncPoint = frameI - 2; 20364 frameI += 3; 20365 break; 20366 20367 case 1: 20368 // skip past non-sync sequences 20369 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 20370 frameI += 3; 20371 break; 20372 } 20373 20374 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 20375 20376 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 20377 foundKeyFrame = true; 20378 } 20379 20380 frameSyncPoint = frameI - 2; 20381 frameI += 3; 20382 break; 20383 20384 default: 20385 // the current byte isn't a one or zero, so it cannot be part 20386 // of a sync sequence 20387 frameI += 3; 20388 break; 20389 } 20390 } 20391 20392 frameBuffer = frameBuffer.subarray(frameSyncPoint); 20393 frameI -= frameSyncPoint; 20394 frameSyncPoint = 0; // parse the final nal 20395 20396 if (frameBuffer && frameBuffer.byteLength > 3) { 20397 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 20398 20399 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 20400 foundKeyFrame = true; 20401 } 20402 } 20403 20404 return foundKeyFrame; 20405 }; 20406 20407 var probe$1 = { 20408 parseType: parseType, 20409 parsePat: parsePat, 20410 parsePmt: parsePmt, 20411 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 20412 parsePesType: parsePesType, 20413 parsePesTime: parsePesTime, 20414 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame 20415 }; 20416 /** 20417 * mux.js 20418 * 20419 * Copyright (c) Brightcove 20420 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 20421 * 20422 * Parse mpeg2 transport stream packets to extract basic timing information 20423 */ 20424 20425 var StreamTypes = streamTypes; 20426 var handleRollover = timestampRolloverStream.handleRollover; 20427 var probe = {}; 20428 probe.ts = probe$1; 20429 probe.aac = utils; 20430 var ONE_SECOND_IN_TS = clock$2.ONE_SECOND_IN_TS; 20431 var MP2T_PACKET_LENGTH = 188, 20432 // bytes 20433 SYNC_BYTE = 0x47; 20434 /** 20435 * walks through segment data looking for pat and pmt packets to parse out 20436 * program map table information 20437 */ 20438 20439 var parsePsi_ = function (bytes, pmt) { 20440 var startIndex = 0, 20441 endIndex = MP2T_PACKET_LENGTH, 20442 packet, 20443 type; 20444 20445 while (endIndex < bytes.byteLength) { 20446 // Look for a pair of start and end sync bytes in the data.. 20447 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 20448 // We found a packet 20449 packet = bytes.subarray(startIndex, endIndex); 20450 type = probe.ts.parseType(packet, pmt.pid); 20451 20452 switch (type) { 20453 case 'pat': 20454 pmt.pid = probe.ts.parsePat(packet); 20455 break; 20456 20457 case 'pmt': 20458 var table = probe.ts.parsePmt(packet); 20459 pmt.table = pmt.table || {}; 20460 Object.keys(table).forEach(function (key) { 20461 pmt.table[key] = table[key]; 20462 }); 20463 break; 20464 } 20465 20466 startIndex += MP2T_PACKET_LENGTH; 20467 endIndex += MP2T_PACKET_LENGTH; 20468 continue; 20469 } // If we get here, we have somehow become de-synchronized and we need to step 20470 // forward one byte at a time until we find a pair of sync bytes that denote 20471 // a packet 20472 20473 20474 startIndex++; 20475 endIndex++; 20476 } 20477 }; 20478 /** 20479 * walks through the segment data from the start and end to get timing information 20480 * for the first and last audio pes packets 20481 */ 20482 20483 20484 var parseAudioPes_ = function (bytes, pmt, result) { 20485 var startIndex = 0, 20486 endIndex = MP2T_PACKET_LENGTH, 20487 packet, 20488 type, 20489 pesType, 20490 pusi, 20491 parsed; 20492 var endLoop = false; // Start walking from start of segment to get first audio packet 20493 20494 while (endIndex <= bytes.byteLength) { 20495 // Look for a pair of start and end sync bytes in the data.. 20496 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 20497 // We found a packet 20498 packet = bytes.subarray(startIndex, endIndex); 20499 type = probe.ts.parseType(packet, pmt.pid); 20500 20501 switch (type) { 20502 case 'pes': 20503 pesType = probe.ts.parsePesType(packet, pmt.table); 20504 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 20505 20506 if (pesType === 'audio' && pusi) { 20507 parsed = probe.ts.parsePesTime(packet); 20508 20509 if (parsed) { 20510 parsed.type = 'audio'; 20511 result.audio.push(parsed); 20512 endLoop = true; 20513 } 20514 } 20515 20516 break; 20517 } 20518 20519 if (endLoop) { 20520 break; 20521 } 20522 20523 startIndex += MP2T_PACKET_LENGTH; 20524 endIndex += MP2T_PACKET_LENGTH; 20525 continue; 20526 } // If we get here, we have somehow become de-synchronized and we need to step 20527 // forward one byte at a time until we find a pair of sync bytes that denote 20528 // a packet 20529 20530 20531 startIndex++; 20532 endIndex++; 20533 } // Start walking from end of segment to get last audio packet 20534 20535 20536 endIndex = bytes.byteLength; 20537 startIndex = endIndex - MP2T_PACKET_LENGTH; 20538 endLoop = false; 20539 20540 while (startIndex >= 0) { 20541 // Look for a pair of start and end sync bytes in the data.. 20542 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 20543 // We found a packet 20544 packet = bytes.subarray(startIndex, endIndex); 20545 type = probe.ts.parseType(packet, pmt.pid); 20546 20547 switch (type) { 20548 case 'pes': 20549 pesType = probe.ts.parsePesType(packet, pmt.table); 20550 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 20551 20552 if (pesType === 'audio' && pusi) { 20553 parsed = probe.ts.parsePesTime(packet); 20554 20555 if (parsed) { 20556 parsed.type = 'audio'; 20557 result.audio.push(parsed); 20558 endLoop = true; 20559 } 20560 } 20561 20562 break; 20563 } 20564 20565 if (endLoop) { 20566 break; 20567 } 20568 20569 startIndex -= MP2T_PACKET_LENGTH; 20570 endIndex -= MP2T_PACKET_LENGTH; 20571 continue; 20572 } // If we get here, we have somehow become de-synchronized and we need to step 20573 // forward one byte at a time until we find a pair of sync bytes that denote 20574 // a packet 20575 20576 20577 startIndex--; 20578 endIndex--; 20579 } 20580 }; 20581 /** 20582 * walks through the segment data from the start and end to get timing information 20583 * for the first and last video pes packets as well as timing information for the first 20584 * key frame. 20585 */ 20586 20587 20588 var parseVideoPes_ = function (bytes, pmt, result) { 20589 var startIndex = 0, 20590 endIndex = MP2T_PACKET_LENGTH, 20591 packet, 20592 type, 20593 pesType, 20594 pusi, 20595 parsed, 20596 frame, 20597 i, 20598 pes; 20599 var endLoop = false; 20600 var currentFrame = { 20601 data: [], 20602 size: 0 20603 }; // Start walking from start of segment to get first video packet 20604 20605 while (endIndex < bytes.byteLength) { 20606 // Look for a pair of start and end sync bytes in the data.. 20607 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 20608 // We found a packet 20609 packet = bytes.subarray(startIndex, endIndex); 20610 type = probe.ts.parseType(packet, pmt.pid); 20611 20612 switch (type) { 20613 case 'pes': 20614 pesType = probe.ts.parsePesType(packet, pmt.table); 20615 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 20616 20617 if (pesType === 'video') { 20618 if (pusi && !endLoop) { 20619 parsed = probe.ts.parsePesTime(packet); 20620 20621 if (parsed) { 20622 parsed.type = 'video'; 20623 result.video.push(parsed); 20624 endLoop = true; 20625 } 20626 } 20627 20628 if (!result.firstKeyFrame) { 20629 if (pusi) { 20630 if (currentFrame.size !== 0) { 20631 frame = new Uint8Array(currentFrame.size); 20632 i = 0; 20633 20634 while (currentFrame.data.length) { 20635 pes = currentFrame.data.shift(); 20636 frame.set(pes, i); 20637 i += pes.byteLength; 20638 } 20639 20640 if (probe.ts.videoPacketContainsKeyFrame(frame)) { 20641 var firstKeyFrame = probe.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting 20642 // the keyframe seems to work fine with HLS playback 20643 // and definitely preferable to a crash with TypeError... 20644 20645 if (firstKeyFrame) { 20646 result.firstKeyFrame = firstKeyFrame; 20647 result.firstKeyFrame.type = 'video'; 20648 } else { 20649 // eslint-disable-next-line 20650 console.warn('Failed to extract PTS/DTS from PES at first keyframe. ' + 'This could be an unusual TS segment, or else mux.js did not ' + 'parse your TS segment correctly. If you know your TS ' + 'segments do contain PTS/DTS on keyframes please file a bug ' + 'report! You can try ffprobe to double check for yourself.'); 20651 } 20652 } 20653 20654 currentFrame.size = 0; 20655 } 20656 } 20657 20658 currentFrame.data.push(packet); 20659 currentFrame.size += packet.byteLength; 20660 } 20661 } 20662 20663 break; 20664 } 20665 20666 if (endLoop && result.firstKeyFrame) { 20667 break; 20668 } 20669 20670 startIndex += MP2T_PACKET_LENGTH; 20671 endIndex += MP2T_PACKET_LENGTH; 20672 continue; 20673 } // If we get here, we have somehow become de-synchronized and we need to step 20674 // forward one byte at a time until we find a pair of sync bytes that denote 20675 // a packet 20676 20677 20678 startIndex++; 20679 endIndex++; 20680 } // Start walking from end of segment to get last video packet 20681 20682 20683 endIndex = bytes.byteLength; 20684 startIndex = endIndex - MP2T_PACKET_LENGTH; 20685 endLoop = false; 20686 20687 while (startIndex >= 0) { 20688 // Look for a pair of start and end sync bytes in the data.. 20689 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 20690 // We found a packet 20691 packet = bytes.subarray(startIndex, endIndex); 20692 type = probe.ts.parseType(packet, pmt.pid); 20693 20694 switch (type) { 20695 case 'pes': 20696 pesType = probe.ts.parsePesType(packet, pmt.table); 20697 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 20698 20699 if (pesType === 'video' && pusi) { 20700 parsed = probe.ts.parsePesTime(packet); 20701 20702 if (parsed) { 20703 parsed.type = 'video'; 20704 result.video.push(parsed); 20705 endLoop = true; 20706 } 20707 } 20708 20709 break; 20710 } 20711 20712 if (endLoop) { 20713 break; 20714 } 20715 20716 startIndex -= MP2T_PACKET_LENGTH; 20717 endIndex -= MP2T_PACKET_LENGTH; 20718 continue; 20719 } // If we get here, we have somehow become de-synchronized and we need to step 20720 // forward one byte at a time until we find a pair of sync bytes that denote 20721 // a packet 20722 20723 20724 startIndex--; 20725 endIndex--; 20726 } 20727 }; 20728 /** 20729 * Adjusts the timestamp information for the segment to account for 20730 * rollover and convert to seconds based on pes packet timescale (90khz clock) 20731 */ 20732 20733 20734 var adjustTimestamp_ = function (segmentInfo, baseTimestamp) { 20735 if (segmentInfo.audio && segmentInfo.audio.length) { 20736 var audioBaseTimestamp = baseTimestamp; 20737 20738 if (typeof audioBaseTimestamp === 'undefined' || isNaN(audioBaseTimestamp)) { 20739 audioBaseTimestamp = segmentInfo.audio[0].dts; 20740 } 20741 20742 segmentInfo.audio.forEach(function (info) { 20743 info.dts = handleRollover(info.dts, audioBaseTimestamp); 20744 info.pts = handleRollover(info.pts, audioBaseTimestamp); // time in seconds 20745 20746 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 20747 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 20748 }); 20749 } 20750 20751 if (segmentInfo.video && segmentInfo.video.length) { 20752 var videoBaseTimestamp = baseTimestamp; 20753 20754 if (typeof videoBaseTimestamp === 'undefined' || isNaN(videoBaseTimestamp)) { 20755 videoBaseTimestamp = segmentInfo.video[0].dts; 20756 } 20757 20758 segmentInfo.video.forEach(function (info) { 20759 info.dts = handleRollover(info.dts, videoBaseTimestamp); 20760 info.pts = handleRollover(info.pts, videoBaseTimestamp); // time in seconds 20761 20762 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 20763 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 20764 }); 20765 20766 if (segmentInfo.firstKeyFrame) { 20767 var frame = segmentInfo.firstKeyFrame; 20768 frame.dts = handleRollover(frame.dts, videoBaseTimestamp); 20769 frame.pts = handleRollover(frame.pts, videoBaseTimestamp); // time in seconds 20770 20771 frame.dtsTime = frame.dts / ONE_SECOND_IN_TS; 20772 frame.ptsTime = frame.pts / ONE_SECOND_IN_TS; 20773 } 20774 } 20775 }; 20776 /** 20777 * inspects the aac data stream for start and end time information 20778 */ 20779 20780 20781 var inspectAac_ = function (bytes) { 20782 var endLoop = false, 20783 audioCount = 0, 20784 sampleRate = null, 20785 timestamp = null, 20786 frameSize = 0, 20787 byteIndex = 0, 20788 packet; 20789 20790 while (bytes.length - byteIndex >= 3) { 20791 var type = probe.aac.parseType(bytes, byteIndex); 20792 20793 switch (type) { 20794 case 'timed-metadata': 20795 // Exit early because we don't have enough to parse 20796 // the ID3 tag header 20797 if (bytes.length - byteIndex < 10) { 20798 endLoop = true; 20799 break; 20800 } 20801 20802 frameSize = probe.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 20803 // to emit a full packet 20804 20805 if (frameSize > bytes.length) { 20806 endLoop = true; 20807 break; 20808 } 20809 20810 if (timestamp === null) { 20811 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 20812 timestamp = probe.aac.parseAacTimestamp(packet); 20813 } 20814 20815 byteIndex += frameSize; 20816 break; 20817 20818 case 'audio': 20819 // Exit early because we don't have enough to parse 20820 // the ADTS frame header 20821 if (bytes.length - byteIndex < 7) { 20822 endLoop = true; 20823 break; 20824 } 20825 20826 frameSize = probe.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 20827 // to emit a full packet 20828 20829 if (frameSize > bytes.length) { 20830 endLoop = true; 20831 break; 20832 } 20833 20834 if (sampleRate === null) { 20835 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 20836 sampleRate = probe.aac.parseSampleRate(packet); 20837 } 20838 20839 audioCount++; 20840 byteIndex += frameSize; 20841 break; 20842 20843 default: 20844 byteIndex++; 20845 break; 20846 } 20847 20848 if (endLoop) { 20849 return null; 20850 } 20851 } 20852 20853 if (sampleRate === null || timestamp === null) { 20854 return null; 20855 } 20856 20857 var audioTimescale = ONE_SECOND_IN_TS / sampleRate; 20858 var result = { 20859 audio: [{ 20860 type: 'audio', 20861 dts: timestamp, 20862 pts: timestamp 20863 }, { 20864 type: 'audio', 20865 dts: timestamp + audioCount * 1024 * audioTimescale, 20866 pts: timestamp + audioCount * 1024 * audioTimescale 20867 }] 20868 }; 20869 return result; 20870 }; 20871 /** 20872 * inspects the transport stream segment data for start and end time information 20873 * of the audio and video tracks (when present) as well as the first key frame's 20874 * start time. 20875 */ 20876 20877 20878 var inspectTs_ = function (bytes) { 20879 var pmt = { 20880 pid: null, 20881 table: null 20882 }; 20883 var result = {}; 20884 parsePsi_(bytes, pmt); 20885 20886 for (var pid in pmt.table) { 20887 if (pmt.table.hasOwnProperty(pid)) { 20888 var type = pmt.table[pid]; 20889 20890 switch (type) { 20891 case StreamTypes.H264_STREAM_TYPE: 20892 result.video = []; 20893 parseVideoPes_(bytes, pmt, result); 20894 20895 if (result.video.length === 0) { 20896 delete result.video; 20897 } 20898 20899 break; 20900 20901 case StreamTypes.ADTS_STREAM_TYPE: 20902 result.audio = []; 20903 parseAudioPes_(bytes, pmt, result); 20904 20905 if (result.audio.length === 0) { 20906 delete result.audio; 20907 } 20908 20909 break; 20910 } 20911 } 20912 } 20913 20914 return result; 20915 }; 20916 /** 20917 * Inspects segment byte data and returns an object with start and end timing information 20918 * 20919 * @param {Uint8Array} bytes The segment byte data 20920 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 20921 * timestamps for rollover. This value must be in 90khz clock. 20922 * @return {Object} Object containing start and end frame timing info of segment. 20923 */ 20924 20925 20926 var inspect = function (bytes, baseTimestamp) { 20927 var isAacData = probe.aac.isLikelyAacData(bytes); 20928 var result; 20929 20930 if (isAacData) { 20931 result = inspectAac_(bytes); 20932 } else { 20933 result = inspectTs_(bytes); 20934 } 20935 20936 if (!result || !result.audio && !result.video) { 20937 return null; 20938 } 20939 20940 adjustTimestamp_(result, baseTimestamp); 20941 return result; 20942 }; 20943 20944 var tsInspector = { 20945 inspect: inspect, 20946 parseAudioPes_: parseAudioPes_ 20947 }; 20948 /* global self */ 20949 20950 /** 20951 * Re-emits transmuxer events by converting them into messages to the 20952 * world outside the worker. 20953 * 20954 * @param {Object} transmuxer the transmuxer to wire events on 20955 * @private 20956 */ 20957 20958 const wireTransmuxerEvents = function (self, transmuxer) { 20959 transmuxer.on('data', function (segment) { 20960 // transfer ownership of the underlying ArrayBuffer 20961 // instead of doing a copy to save memory 20962 // ArrayBuffers are transferable but generic TypedArrays are not 20963 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 20964 const initArray = segment.initSegment; 20965 segment.initSegment = { 20966 data: initArray.buffer, 20967 byteOffset: initArray.byteOffset, 20968 byteLength: initArray.byteLength 20969 }; 20970 const typedArray = segment.data; 20971 segment.data = typedArray.buffer; 20972 self.postMessage({ 20973 action: 'data', 20974 segment, 20975 byteOffset: typedArray.byteOffset, 20976 byteLength: typedArray.byteLength 20977 }, [segment.data]); 20978 }); 20979 transmuxer.on('done', function (data) { 20980 self.postMessage({ 20981 action: 'done' 20982 }); 20983 }); 20984 transmuxer.on('gopInfo', function (gopInfo) { 20985 self.postMessage({ 20986 action: 'gopInfo', 20987 gopInfo 20988 }); 20989 }); 20990 transmuxer.on('videoSegmentTimingInfo', function (timingInfo) { 20991 const videoSegmentTimingInfo = { 20992 start: { 20993 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 20994 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts) 20995 }, 20996 end: { 20997 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 20998 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts) 20999 }, 21000 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime) 21001 }; 21002 21003 if (timingInfo.prependedContentDuration) { 21004 videoSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration); 21005 } 21006 21007 self.postMessage({ 21008 action: 'videoSegmentTimingInfo', 21009 videoSegmentTimingInfo 21010 }); 21011 }); 21012 transmuxer.on('audioSegmentTimingInfo', function (timingInfo) { 21013 // Note that all times for [audio/video]SegmentTimingInfo events are in video clock 21014 const audioSegmentTimingInfo = { 21015 start: { 21016 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 21017 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts) 21018 }, 21019 end: { 21020 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 21021 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts) 21022 }, 21023 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime) 21024 }; 21025 21026 if (timingInfo.prependedContentDuration) { 21027 audioSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration); 21028 } 21029 21030 self.postMessage({ 21031 action: 'audioSegmentTimingInfo', 21032 audioSegmentTimingInfo 21033 }); 21034 }); 21035 transmuxer.on('id3Frame', function (id3Frame) { 21036 self.postMessage({ 21037 action: 'id3Frame', 21038 id3Frame 21039 }); 21040 }); 21041 transmuxer.on('caption', function (caption) { 21042 self.postMessage({ 21043 action: 'caption', 21044 caption 21045 }); 21046 }); 21047 transmuxer.on('trackinfo', function (trackInfo) { 21048 self.postMessage({ 21049 action: 'trackinfo', 21050 trackInfo 21051 }); 21052 }); 21053 transmuxer.on('audioTimingInfo', function (audioTimingInfo) { 21054 // convert to video TS since we prioritize video time over audio 21055 self.postMessage({ 21056 action: 'audioTimingInfo', 21057 audioTimingInfo: { 21058 start: clock$2.videoTsToSeconds(audioTimingInfo.start), 21059 end: clock$2.videoTsToSeconds(audioTimingInfo.end) 21060 } 21061 }); 21062 }); 21063 transmuxer.on('videoTimingInfo', function (videoTimingInfo) { 21064 self.postMessage({ 21065 action: 'videoTimingInfo', 21066 videoTimingInfo: { 21067 start: clock$2.videoTsToSeconds(videoTimingInfo.start), 21068 end: clock$2.videoTsToSeconds(videoTimingInfo.end) 21069 } 21070 }); 21071 }); 21072 transmuxer.on('log', function (log) { 21073 self.postMessage({ 21074 action: 'log', 21075 log 21076 }); 21077 }); 21078 }; 21079 /** 21080 * All incoming messages route through this hash. If no function exists 21081 * to handle an incoming message, then we ignore the message. 21082 * 21083 * @class MessageHandlers 21084 * @param {Object} options the options to initialize with 21085 */ 21086 21087 21088 class MessageHandlers { 21089 constructor(self, options) { 21090 this.options = options || {}; 21091 this.self = self; 21092 this.init(); 21093 } 21094 /** 21095 * initialize our web worker and wire all the events. 21096 */ 21097 21098 21099 init() { 21100 if (this.transmuxer) { 21101 this.transmuxer.dispose(); 21102 } 21103 21104 this.transmuxer = new transmuxer.Transmuxer(this.options); 21105 wireTransmuxerEvents(this.self, this.transmuxer); 21106 } 21107 21108 pushMp4Captions(data) { 21109 if (!this.captionParser) { 21110 this.captionParser = new captionParser(); 21111 this.captionParser.init(); 21112 } 21113 21114 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 21115 const parsed = this.captionParser.parse(segment, data.trackIds, data.timescales); 21116 this.self.postMessage({ 21117 action: 'mp4Captions', 21118 captions: parsed && parsed.captions || [], 21119 logs: parsed && parsed.logs || [], 21120 data: segment.buffer 21121 }, [segment.buffer]); 21122 } 21123 /** 21124 * Initializes the WebVttParser and passes the init segment. 21125 * 21126 * @param {Uint8Array} data mp4 boxed WebVTT init segment data 21127 */ 21128 21129 21130 initMp4WebVttParser(data) { 21131 if (!this.webVttParser) { 21132 this.webVttParser = new webvttParser(); 21133 } 21134 21135 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); // Set the timescale for the parser. 21136 // This can be called repeatedly in order to set and re-set the timescale. 21137 21138 this.webVttParser.init(segment); 21139 } 21140 /** 21141 * Parse an mp4 encapsulated WebVTT segment and return an array of cues. 21142 * 21143 * @param {Uint8Array} data a text/webvtt segment 21144 * @return {Object[]} an array of parsed cue objects 21145 */ 21146 21147 21148 getMp4WebVttText(data) { 21149 if (!this.webVttParser) { 21150 // timescale might not be set yet if the parser is created before an init segment is passed. 21151 // default timescale is 90k. 21152 this.webVttParser = new webvttParser(); 21153 } 21154 21155 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 21156 const parsed = this.webVttParser.parseSegment(segment); 21157 this.self.postMessage({ 21158 action: 'getMp4WebVttText', 21159 mp4VttCues: parsed || [], 21160 data: segment.buffer 21161 }, [segment.buffer]); 21162 } 21163 21164 probeMp4StartTime({ 21165 timescales, 21166 data 21167 }) { 21168 const startTime = probe$2.startTime(timescales, data); 21169 this.self.postMessage({ 21170 action: 'probeMp4StartTime', 21171 startTime, 21172 data 21173 }, [data.buffer]); 21174 } 21175 21176 probeMp4Tracks({ 21177 data 21178 }) { 21179 const tracks = probe$2.tracks(data); 21180 this.self.postMessage({ 21181 action: 'probeMp4Tracks', 21182 tracks, 21183 data 21184 }, [data.buffer]); 21185 } 21186 /** 21187 * Probes an mp4 segment for EMSG boxes containing ID3 data. 21188 * https://aomediacodec.github.io/id3-emsg/ 21189 * 21190 * @param {Uint8Array} data segment data 21191 * @param {number} offset segment start time 21192 * @return {Object[]} an array of ID3 frames 21193 */ 21194 21195 21196 probeEmsgID3({ 21197 data, 21198 offset 21199 }) { 21200 const id3Frames = probe$2.getEmsgID3(data, offset); 21201 this.self.postMessage({ 21202 action: 'probeEmsgID3', 21203 id3Frames, 21204 emsgData: data 21205 }, [data.buffer]); 21206 } 21207 /** 21208 * Probe an mpeg2-ts segment to determine the start time of the segment in it's 21209 * internal "media time," as well as whether it contains video and/or audio. 21210 * 21211 * @private 21212 * @param {Uint8Array} bytes - segment bytes 21213 * @param {number} baseStartTime 21214 * Relative reference timestamp used when adjusting frame timestamps for rollover. 21215 * This value should be in seconds, as it's converted to a 90khz clock within the 21216 * function body. 21217 * @return {Object} The start time of the current segment in "media time" as well as 21218 * whether it contains video and/or audio 21219 */ 21220 21221 21222 probeTs({ 21223 data, 21224 baseStartTime 21225 }) { 21226 const tsStartTime = typeof baseStartTime === 'number' && !isNaN(baseStartTime) ? baseStartTime * clock$2.ONE_SECOND_IN_TS : void 0; 21227 const timeInfo = tsInspector.inspect(data, tsStartTime); 21228 let result = null; 21229 21230 if (timeInfo) { 21231 result = { 21232 // each type's time info comes back as an array of 2 times, start and end 21233 hasVideo: timeInfo.video && timeInfo.video.length === 2 || false, 21234 hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false 21235 }; 21236 21237 if (result.hasVideo) { 21238 result.videoStart = timeInfo.video[0].ptsTime; 21239 } 21240 21241 if (result.hasAudio) { 21242 result.audioStart = timeInfo.audio[0].ptsTime; 21243 } 21244 } 21245 21246 this.self.postMessage({ 21247 action: 'probeTs', 21248 result, 21249 data 21250 }, [data.buffer]); 21251 } 21252 21253 clearAllMp4Captions() { 21254 if (this.captionParser) { 21255 this.captionParser.clearAllCaptions(); 21256 } 21257 } 21258 21259 clearParsedMp4Captions() { 21260 if (this.captionParser) { 21261 this.captionParser.clearParsedCaptions(); 21262 } 21263 } 21264 /** 21265 * Adds data (a ts segment) to the start of the transmuxer pipeline for 21266 * processing. 21267 * 21268 * @param {ArrayBuffer} data data to push into the muxer 21269 */ 21270 21271 21272 push(data) { 21273 // Cast array buffer to correct type for transmuxer 21274 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 21275 this.transmuxer.push(segment); 21276 } 21277 /** 21278 * Recreate the transmuxer so that the next segment added via `push` 21279 * start with a fresh transmuxer. 21280 */ 21281 21282 21283 reset() { 21284 this.transmuxer.reset(); 21285 } 21286 /** 21287 * Set the value that will be used as the `baseMediaDecodeTime` time for the 21288 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 21289 * set relative to the first based on the PTS values. 21290 * 21291 * @param {Object} data used to set the timestamp offset in the muxer 21292 */ 21293 21294 21295 setTimestampOffset(data) { 21296 const timestampOffset = data.timestampOffset || 0; 21297 this.transmuxer.setBaseMediaDecodeTime(Math.round(clock$2.secondsToVideoTs(timestampOffset))); 21298 } 21299 21300 setAudioAppendStart(data) { 21301 this.transmuxer.setAudioAppendStart(Math.ceil(clock$2.secondsToVideoTs(data.appendStart))); 21302 } 21303 21304 setRemux(data) { 21305 this.transmuxer.setRemux(data.remux); 21306 } 21307 /** 21308 * Forces the pipeline to finish processing the last segment and emit it's 21309 * results. 21310 * 21311 * @param {Object} data event data, not really used 21312 */ 21313 21314 21315 flush(data) { 21316 this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed 21317 21318 self.postMessage({ 21319 action: 'done', 21320 type: 'transmuxed' 21321 }); 21322 } 21323 21324 endTimeline() { 21325 this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their 21326 // timelines 21327 21328 self.postMessage({ 21329 action: 'endedtimeline', 21330 type: 'transmuxed' 21331 }); 21332 } 21333 21334 alignGopsWith(data) { 21335 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 21336 } 21337 21338 } 21339 /** 21340 * Our web worker interface so that things can talk to mux.js 21341 * that will be running in a web worker. the scope is passed to this by 21342 * webworkify. 21343 * 21344 * @param {Object} self the scope for the web worker 21345 */ 21346 21347 21348 self.onmessage = function (event) { 21349 if (event.data.action === 'init' && event.data.options) { 21350 this.messageHandlers = new MessageHandlers(self, event.data.options); 21351 return; 21352 } 21353 21354 if (!this.messageHandlers) { 21355 this.messageHandlers = new MessageHandlers(self); 21356 } 21357 21358 if (event.data && event.data.action && event.data.action !== 'init') { 21359 if (this.messageHandlers[event.data.action]) { 21360 this.messageHandlers[event.data.action](event.data); 21361 } 21362 } 21363 }; 21364 })); 21365 var TransmuxWorker = factory(workerCode$1); 21366 /* rollup-plugin-worker-factory end for worker!/
21366home/runner/work/http-streaming
vendor: 98,517 bytes, lines 21366-24328
21366/http-streaming/src/transmuxer-worker.js */ 21367 21368 const handleData_ = (event, transmuxedData, callback) => { 21369 const { 21370 type, 21371 initSegment, 21372 captions, 21373 captionStreams, 21374 metadata, 21375 videoFrameDtsTime, 21376 videoFramePtsTime 21377 } = event.data.segment; 21378 transmuxedData.buffer.push({ 21379 captions, 21380 captionStreams, 21381 metadata 21382 }); 21383 const boxes = event.data.segment.boxes || { 21384 data: event.data.segment.data 21385 }; 21386 const result = { 21387 type, 21388 // cast ArrayBuffer to TypedArray 21389 data: new Uint8Array(boxes.data, boxes.data.byteOffset, boxes.data.byteLength), 21390 initSegment: new Uint8Array(initSegment.data, initSegment.byteOffset, initSegment.byteLength) 21391 }; 21392 21393 if (typeof videoFrameDtsTime !== 'undefined') { 21394 result.videoFrameDtsTime = videoFrameDtsTime; 21395 } 21396 21397 if (typeof videoFramePtsTime !== 'undefined') { 21398 result.videoFramePtsTime = videoFramePtsTime; 21399 } 21400 21401 callback(result); 21402 }; 21403 const handleDone_ = ({ 21404 transmuxedData, 21405 callback 21406 }) => { 21407 // Previously we only returned data on data events, 21408 // not on done events. Clear out the buffer to keep that consistent. 21409 transmuxedData.buffer = []; // all buffers should have been flushed from the muxer, so start processing anything we 21410 // have received 21411 21412 callback(transmuxedData); 21413 }; 21414 const handleGopInfo_ = (event, transmuxedData) => { 21415 transmuxedData.gopInfo = event.data.gopInfo; 21416 }; 21417 const processTransmux = options => { 21418 const { 21419 transmuxer, 21420 bytes, 21421 audioAppendStart, 21422 gopsToAlignWith, 21423 remux, 21424 onData, 21425 onTrackInfo, 21426 onAudioTimingInfo, 21427 onVideoTimingInfo, 21428 onVideoSegmentTimingInfo, 21429 onAudioSegmentTimingInfo, 21430 onId3, 21431 onCaptions, 21432 onDone, 21433 onEndedTimeline, 21434 onTransmuxerLog, 21435 isEndOfTimeline, 21436 segment, 21437 triggerSegmentEventFn 21438 } = options; 21439 const transmuxedData = { 21440 buffer: [] 21441 }; 21442 let waitForEndedTimelineEvent = isEndOfTimeline; 21443 21444 const handleMessage = event => { 21445 if (transmuxer.currentTransmux !== options) { 21446 // disposed 21447 return; 21448 } 21449 21450 if (event.data.action === 'data') { 21451 handleData_(event, transmuxedData, onData); 21452 } 21453 21454 if (event.data.action === 'trackinfo') { 21455 onTrackInfo(event.data.trackInfo); 21456 } 21457 21458 if (event.data.action === 'gopInfo') { 21459 handleGopInfo_(event, transmuxedData); 21460 } 21461 21462 if (event.data.action === 'audioTimingInfo') { 21463 onAudioTimingInfo(event.data.audioTimingInfo); 21464 } 21465 21466 if (event.data.action === 'videoTimingInfo') { 21467 onVideoTimingInfo(event.data.videoTimingInfo); 21468 } 21469 21470 if (event.data.action === 'videoSegmentTimingInfo') { 21471 onVideoSegmentTimingInfo(event.data.videoSegmentTimingInfo); 21472 } 21473 21474 if (event.data.action === 'audioSegmentTimingInfo') { 21475 onAudioSegmentTimingInfo(event.data.audioSegmentTimingInfo); 21476 } 21477 21478 if (event.data.action === 'id3Frame') { 21479 onId3([event.data.id3Frame], event.data.id3Frame.dispatchType); 21480 } 21481 21482 if (event.data.action === 'caption') { 21483 onCaptions(event.data.caption); 21484 } 21485 21486 if (event.data.action === 'endedtimeline') { 21487 waitForEndedTimelineEvent = false; 21488 onEndedTimeline(); 21489 } 21490 21491 if (event.data.action === 'log') { 21492 onTransmuxerLog(event.data.log); 21493 } // wait for the transmuxed event since we may have audio and video 21494 21495 21496 if (event.data.type !== 'transmuxed') { 21497 return; 21498 } // If the "endedtimeline" event has not yet fired, and this segment represents the end 21499 // of a timeline, that means there may still be data events before the segment 21500 // processing can be considerred complete. In that case, the final event should be 21501 // an "endedtimeline" event with the type "transmuxed." 21502 21503 21504 if (waitForEndedTimelineEvent) { 21505 return; 21506 } 21507 21508 transmuxer.onmessage = null; 21509 handleDone_({ 21510 transmuxedData, 21511 callback: onDone 21512 }); 21513 /* eslint-disable no-use-before-define */ 21514 21515 dequeue(transmuxer); 21516 /* eslint-enable */ 21517 }; 21518 21519 const handleError = () => { 21520 const error = { 21521 message: 'Received an error message from the transmuxer worker', 21522 metadata: { 21523 errorType: videojs__default["default"].Error.StreamingFailedToTransmuxSegment, 21524 segmentInfo: segmentInfoPayload({ 21525 segment 21526 }) 21527 } 21528 }; 21529 onDone(null, error); 21530 }; 21531 21532 transmuxer.onmessage = handleMessage; 21533 transmuxer.onerror = handleError; 21534 21535 if (audioAppendStart) { 21536 transmuxer.postMessage({ 21537 action: 'setAudioAppendStart', 21538 appendStart: audioAppendStart 21539 }); 21540 } // allow empty arrays to be passed to clear out GOPs 21541 21542 21543 if (Array.isArray(gopsToAlignWith)) { 21544 transmuxer.postMessage({ 21545 action: 'alignGopsWith', 21546 gopsToAlignWith 21547 }); 21548 } 21549 21550 if (typeof remux !== 'undefined') { 21551 transmuxer.postMessage({ 21552 action: 'setRemux', 21553 remux 21554 }); 21555 } 21556 21557 if (bytes.byteLength) { 21558 const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; 21559 const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; 21560 triggerSegmentEventFn({ 21561 type: 'segmenttransmuxingstart', 21562 segment 21563 }); 21564 transmuxer.postMessage({ 21565 action: 'push', 21566 // Send the typed-array of data as an ArrayBuffer so that 21567 // it can be sent as a "Transferable" and avoid the costly 21568 // memory copy 21569 data: buffer, 21570 // To recreate the original typed-array, we need information 21571 // about what portion of the ArrayBuffer it was a view into 21572 byteOffset, 21573 byteLength: bytes.byteLength 21574 }, [buffer]); 21575 } 21576 21577 if (isEndOfTimeline) { 21578 transmuxer.postMessage({ 21579 action: 'endTimeline' 21580 }); 21581 } // even if we didn't push any bytes, we have to make sure we flush in case we reached 21582 // the end of the segment 21583 21584 21585 transmuxer.postMessage({ 21586 action: 'flush' 21587 }); 21588 }; 21589 const dequeue = transmuxer => { 21590 transmuxer.currentTransmux = null; 21591 21592 if (transmuxer.transmuxQueue.length) { 21593 transmuxer.currentTransmux = transmuxer.transmuxQueue.shift(); 21594 21595 if (typeof transmuxer.currentTransmux === 'function') { 21596 transmuxer.currentTransmux(); 21597 } else { 21598 processTransmux(transmuxer.currentTransmux); 21599 } 21600 } 21601 }; 21602 const processAction = (transmuxer, action) => { 21603 transmuxer.postMessage({ 21604 action 21605 }); 21606 dequeue(transmuxer); 21607 }; 21608 const enqueueAction = (action, transmuxer) => { 21609 if (!transmuxer.currentTransmux) { 21610 transmuxer.currentTransmux = action; 21611 processAction(transmuxer, action); 21612 return; 21613 } 21614 21615 transmuxer.transmuxQueue.push(processAction.bind(null, transmuxer, action)); 21616 }; 21617 const reset = transmuxer => { 21618 enqueueAction('reset', transmuxer); 21619 }; 21620 const endTimeline = transmuxer => { 21621 enqueueAction('endTimeline', transmuxer); 21622 }; 21623 const transmux = options => { 21624 if (!options.transmuxer.currentTransmux) { 21625 options.transmuxer.currentTransmux = options; 21626 processTransmux(options); 21627 return; 21628 } 21629 21630 options.transmuxer.transmuxQueue.push(options); 21631 }; 21632 const createTransmuxer = options => { 21633 const transmuxer = new TransmuxWorker(); 21634 transmuxer.currentTransmux = null; 21635 transmuxer.transmuxQueue = []; 21636 const term = transmuxer.terminate; 21637 21638 transmuxer.terminate = () => { 21639 transmuxer.currentTransmux = null; 21640 transmuxer.transmuxQueue.length = 0; 21641 return term.call(transmuxer); 21642 }; 21643 21644 transmuxer.postMessage({ 21645 action: 'init', 21646 options 21647 }); 21648 return transmuxer; 21649 }; 21650 var segmentTransmuxer = { 21651 reset, 21652 endTimeline, 21653 transmux, 21654 createTransmuxer 21655 }; 21656 21657 const workerCallback = function (options) { 21658 const transmuxer = options.transmuxer; 21659 const endAction = options.endAction || options.action; 21660 const callback = options.callback; 21661 21662 const message = _extends({}, options, { 21663 endAction: null, 21664 transmuxer: null, 21665 callback: null 21666 }); 21667 21668 const listenForEndEvent = event => { 21669 if (event.data.action !== endAction) { 21670 return; 21671 } 21672 21673 transmuxer.removeEventListener('message', listenForEndEvent); // transfer ownership of bytes back to us. 21674 21675 if (event.data.data) { 21676 event.data.data = new Uint8Array(event.data.data, options.byteOffset || 0, options.byteLength || event.data.data.byteLength); 21677 21678 if (options.data) { 21679 options.data = event.data.data; 21680 } 21681 } 21682 21683 callback(event.data); 21684 }; 21685 21686 transmuxer.addEventListener('message', listenForEndEvent); 21687 21688 if (options.data) { 21689 const isArrayBuffer = options.data instanceof ArrayBuffer; 21690 message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset; 21691 message.byteLength = options.data.byteLength; 21692 const transfers = [isArrayBuffer ? options.data : options.data.buffer]; 21693 transmuxer.postMessage(message, transfers); 21694 } else { 21695 transmuxer.postMessage(message); 21696 } 21697 }; 21698 21699 const REQUEST_ERRORS = { 21700 FAILURE: 2, 21701 TIMEOUT: -101, 21702 ABORTED: -102 21703 }; 21704 const WEB_VTT_CODEC = 'wvtt'; 21705 /** 21706 * Abort all requests 21707 * 21708 * @param {Object} activeXhrs - an object that tracks all XHR requests 21709 */ 21710 21711 const abortAll = activeXhrs => { 21712 activeXhrs.forEach(xhr => { 21713 xhr.abort(); 21714 }); 21715 }; 21716 /** 21717 * Gather important bandwidth stats once a request has completed 21718 * 21719 * @param {Object} request - the XHR request from which to gather stats 21720 */ 21721 21722 21723 const getRequestStats = request => { 21724 return { 21725 bandwidth: request.bandwidth, 21726 bytesReceived: request.bytesReceived || 0, 21727 roundTripTime: request.roundTripTime || 0 21728 }; 21729 }; 21730 /** 21731 * If possible gather bandwidth stats as a request is in 21732 * progress 21733 * 21734 * @param {Event} progressEvent - an event object from an XHR's progress event 21735 */ 21736 21737 21738 const getProgressStats = progressEvent => { 21739 const request = progressEvent.target; 21740 const roundTripTime = Date.now() - request.requestTime; 21741 const stats = { 21742 bandwidth: Infinity, 21743 bytesReceived: 0, 21744 roundTripTime: roundTripTime || 0 21745 }; 21746 stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok 21747 // because we should only use bandwidth stats on progress to determine when 21748 // abort a request early due to insufficient bandwidth 21749 21750 stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); 21751 return stats; 21752 }; 21753 /** 21754 * Handle all error conditions in one place and return an object 21755 * with all the information 21756 * 21757 * @param {Error|null} error - if non-null signals an error occured with the XHR 21758 * @param {Object} request - the XHR request that possibly generated the error 21759 */ 21760 21761 21762 const handleErrors = (error, request) => { 21763 const { 21764 requestType 21765 } = request; 21766 const metadata = getStreamingNetworkErrorMetadata({ 21767 requestType, 21768 request, 21769 error 21770 }); 21771 21772 if (request.timedout) { 21773 return { 21774 status: request.status, 21775 message: 'HLS request timed-out at URL: ' + request.uri, 21776 code: REQUEST_ERRORS.TIMEOUT, 21777 xhr: request, 21778 metadata 21779 }; 21780 } 21781 21782 if (request.aborted) { 21783 return { 21784 status: request.status, 21785 message: 'HLS request aborted at URL: ' + request.uri, 21786 code: REQUEST_ERRORS.ABORTED, 21787 xhr: request, 21788 metadata 21789 }; 21790 } 21791 21792 if (error) { 21793 return { 21794 status: request.status, 21795 message: 'HLS request errored at URL: ' + request.uri, 21796 code: REQUEST_ERRORS.FAILURE, 21797 xhr: request, 21798 metadata 21799 }; 21800 } 21801 21802 if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) { 21803 return { 21804 status: request.status, 21805 message: 'Empty HLS response at URL: ' + request.uri, 21806 code: REQUEST_ERRORS.FAILURE, 21807 xhr: request, 21808 metadata 21809 }; 21810 } 21811 21812 return null; 21813 }; 21814 /** 21815 * Handle responses for key data and convert the key data to the correct format 21816 * for the decryption step later 21817 * 21818 * @param {Object} segment - a simplified copy of the segmentInfo object 21819 * from SegmentLoader 21820 * @param {Array} objects - objects to add the key bytes to. 21821 * @param {Function} finishProcessingFn - a callback to execute to continue processing 21822 * this request 21823 */ 21824 21825 21826 const handleKeyResponse = (segment, objects, finishProcessingFn, triggerSegmentEventFn) => (error, request) => { 21827 const response = request.response; 21828 const errorObj = handleErrors(error, request); 21829 21830 if (errorObj) { 21831 return finishProcessingFn(errorObj, segment); 21832 } 21833 21834 if (response.byteLength !== 16) { 21835 return finishProcessingFn({ 21836 status: request.status, 21837 message: 'Invalid HLS key at URL: ' + request.uri, 21838 code: REQUEST_ERRORS.FAILURE, 21839 xhr: request 21840 }, segment); 21841 } 21842 21843 const view = new DataView(response); 21844 const bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); 21845 21846 for (let i = 0; i < objects.length; i++) { 21847 objects[i].bytes = bytes; 21848 } 21849 21850 const keyInfo = { 21851 uri: request.uri 21852 }; 21853 triggerSegmentEventFn({ 21854 type: 'segmentkeyloadcomplete', 21855 segment, 21856 keyInfo 21857 }); 21858 return finishProcessingFn(null, segment); 21859 }; 21860 /** 21861 * Processes an mp4 init segment depending on the codec through the transmuxer. 21862 * 21863 * @param {Object} segment init segment to process 21864 * @param {string} codec the codec of the text segments 21865 */ 21866 21867 21868 const initMp4Text = (segment, codec) => { 21869 if (codec === WEB_VTT_CODEC) { 21870 segment.transmuxer.postMessage({ 21871 action: 'initMp4WebVttParser', 21872 data: segment.map.bytes 21873 }); 21874 } 21875 }; 21876 /** 21877 * Parses an mp4 text segment with the transmuxer and calls the doneFn from 21878 * the segment loader. 21879 * 21880 * @param {Object} segment the text segment to parse 21881 * @param {string} codec the codec of the text segment 21882 * @param {Function} doneFn the doneFn passed from the segment loader 21883 */ 21884 21885 21886 const parseMp4TextSegment = (segment, codec, doneFn) => { 21887 if (codec === WEB_VTT_CODEC) { 21888 workerCallback({ 21889 action: 'getMp4WebVttText', 21890 data: segment.bytes, 21891 transmuxer: segment.transmuxer, 21892 callback: ({ 21893 data, 21894 mp4VttCues 21895 }) => { 21896 segment.bytes = data; 21897 doneFn(null, segment, { 21898 mp4VttCues 21899 }); 21900 } 21901 }); 21902 } 21903 }; 21904 21905 const parseInitSegment = (segment, callback) => { 21906 const type = detectContainerForBytes(segment.map.bytes); // TODO: We should also handle ts init segments here, but we 21907 // only know how to parse mp4 init segments at the moment 21908 21909 if (type !== 'mp4') { 21910 const uri = segment.map.resolvedUri || segment.map.uri; 21911 const mediaType = type || 'unknown'; 21912 return callback({ 21913 internal: true, 21914 message: `Found unsupported ${mediaType} container for initialization segment at URL: ${uri}`, 21915 code: REQUEST_ERRORS.FAILURE, 21916 metadata: { 21917 mediaType 21918 } 21919 }); 21920 } 21921 21922 workerCallback({ 21923 action: 'probeMp4Tracks', 21924 data: segment.map.bytes, 21925 transmuxer: segment.transmuxer, 21926 callback: ({ 21927 tracks, 21928 data 21929 }) => { 21930 // transfer bytes back to us 21931 segment.map.bytes = data; 21932 tracks.forEach(function (track) { 21933 segment.map.tracks = segment.map.tracks || {}; // only support one track of each type for now 21934 21935 if (segment.map.tracks[track.type]) { 21936 return; 21937 } 21938 21939 segment.map.tracks[track.type] = track; 21940 21941 if (typeof track.id === 'number' && track.timescale) { 21942 segment.map.timescales = segment.map.timescales || {}; 21943 segment.map.timescales[track.id] = track.timescale; 21944 } 21945 21946 if (track.type === 'text') { 21947 initMp4Text(segment, track.codec); 21948 } 21949 }); 21950 return callback(null); 21951 } 21952 }); 21953 }; 21954 /** 21955 * Handle init-segment responses 21956 * 21957 * @param {Object} segment - a simplified copy of the segmentInfo object 21958 * from SegmentLoader 21959 * @param {Function} finishProcessingFn - a callback to execute to continue processing 21960 * this request 21961 */ 21962 21963 21964 const handleInitSegmentResponse = ({ 21965 segment, 21966 finishProcessingFn, 21967 triggerSegmentEventFn 21968 }) => (error, request) => { 21969 const errorObj = handleErrors(error, request); 21970 21971 if (errorObj) { 21972 return finishProcessingFn(errorObj, segment); 21973 } 21974 21975 const bytes = new Uint8Array(request.response); 21976 triggerSegmentEventFn({ 21977 type: 'segmentloaded', 21978 segment 21979 }); // init segment is encypted, we will have to wait 21980 // until the key request is done to decrypt. 21981 21982 if (segment.map.key) { 21983 segment.map.encryptedBytes = bytes; 21984 return finishProcessingFn(null, segment); 21985 } 21986 21987 segment.map.bytes = bytes; 21988 parseInitSegment(segment, function (parseError) { 21989 if (parseError) { 21990 parseError.xhr = request; 21991 parseError.status = request.status; 21992 return finishProcessingFn(parseError, segment); 21993 } 21994 21995 finishProcessingFn(null, segment); 21996 }); 21997 }; 21998 /** 21999 * Response handler for segment-requests being sure to set the correct 22000 * property depending on whether the segment is encryped or not 22001 * Also records and keeps track of stats that are used for ABR purposes 22002 * 22003 * @param {Object} segment - a simplified copy of the segmentInfo object 22004 * from SegmentLoader 22005 * @param {Function} finishProcessingFn - a callback to execute to continue processing 22006 * this request 22007 */ 22008 22009 22010 const handleSegmentResponse = ({ 22011 segment, 22012 finishProcessingFn, 22013 responseType, 22014 triggerSegmentEventFn 22015 }) => (error, request) => { 22016 const errorObj = handleErrors(error, request); 22017 22018 if (errorObj) { 22019 return finishProcessingFn(errorObj, segment); 22020 } 22021 22022 triggerSegmentEventFn({ 22023 type: 'segmentloaded', 22024 segment 22025 }); 22026 const newBytes = // although responseText "should" exist, this guard serves to prevent an error being 22027 // thrown for two primary cases: 22028 // 1. the mime type override stops working, or is not implemented for a specific 22029 // browser 22030 // 2. when using mock XHR libraries like sinon that do not allow the override behavior 22031 responseType === 'arraybuffer' || !request.responseText ? request.response : stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0)); 22032 segment.stats = getRequestStats(request); 22033 22034 if (segment.key) { 22035 segment.encryptedBytes = new Uint8Array(newBytes); 22036 } else { 22037 segment.bytes = new Uint8Array(newBytes); 22038 } 22039 22040 return finishProcessingFn(null, segment); 22041 }; 22042 22043 const transmuxAndNotify = ({ 22044 segment, 22045 bytes, 22046 trackInfoFn, 22047 timingInfoFn, 22048 videoSegmentTimingInfoFn, 22049 audioSegmentTimingInfoFn, 22050 id3Fn, 22051 captionsFn, 22052 isEndOfTimeline, 22053 endedTimelineFn, 22054 dataFn, 22055 doneFn, 22056 onTransmuxerLog, 22057 triggerSegmentEventFn 22058 }) => { 22059 const fmp4Tracks = segment.map && segment.map.tracks || {}; 22060 const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); // Keep references to each function so we can null them out after we're done with them. 22061 // One reason for this is that in the case of full segments, we want to trust start 22062 // times from the probe, rather than the transmuxer. 22063 22064 let audioStartFn = timingInfoFn.bind(null, segment, 'audio', 'start'); 22065 const audioEndFn = timingInfoFn.bind(null, segment, 'audio', 'end'); 22066 let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start'); 22067 const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end'); 22068 22069 const finish = () => transmux({ 22070 bytes, 22071 transmuxer: segment.transmuxer, 22072 audioAppendStart: segment.audioAppendStart, 22073 gopsToAlignWith: segment.gopsToAlignWith, 22074 remux: isMuxed, 22075 onData: result => { 22076 result.type = result.type === 'combined' ? 'video' : result.type; 22077 dataFn(segment, result); 22078 }, 22079 onTrackInfo: trackInfo => { 22080 if (trackInfoFn) { 22081 if (isMuxed) { 22082 trackInfo.isMuxed = true; 22083 } 22084 22085 trackInfoFn(segment, trackInfo); 22086 } 22087 }, 22088 onAudioTimingInfo: audioTimingInfo => { 22089 // we only want the first start value we encounter 22090 if (audioStartFn && typeof audioTimingInfo.start !== 'undefined') { 22091 audioStartFn(audioTimingInfo.start); 22092 audioStartFn = null; 22093 } // we want to continually update the end time 22094 22095 22096 if (audioEndFn && typeof audioTimingInfo.end !== 'undefined') { 22097 audioEndFn(audioTimingInfo.end); 22098 } 22099 }, 22100 onVideoTimingInfo: videoTimingInfo => { 22101 // we only want the first start value we encounter 22102 if (videoStartFn && typeof videoTimingInfo.start !== 'undefined') { 22103 videoStartFn(videoTimingInfo.start); 22104 videoStartFn = null; 22105 } // we want to continually update the end time 22106 22107 22108 if (videoEndFn && typeof videoTimingInfo.end !== 'undefined') { 22109 videoEndFn(videoTimingInfo.end); 22110 } 22111 }, 22112 onVideoSegmentTimingInfo: videoSegmentTimingInfo => { 22113 const timingInfo = { 22114 pts: { 22115 start: videoSegmentTimingInfo.start.presentation, 22116 end: videoSegmentTimingInfo.end.presentation 22117 }, 22118 dts: { 22119 start: videoSegmentTimingInfo.start.decode, 22120 end: videoSegmentTimingInfo.end.decode 22121 } 22122 }; 22123 triggerSegmentEventFn({ 22124 type: 'segmenttransmuxingtiminginfoavailable', 22125 segment, 22126 timingInfo 22127 }); 22128 videoSegmentTimingInfoFn(videoSegmentTimingInfo); 22129 }, 22130 onAudioSegmentTimingInfo: audioSegmentTimingInfo => { 22131 const timingInfo = { 22132 pts: { 22133 start: audioSegmentTimingInfo.start.pts, 22134 end: audioSegmentTimingInfo.end.pts 22135 }, 22136 dts: { 22137 start: audioSegmentTimingInfo.start.dts, 22138 end: audioSegmentTimingInfo.end.dts 22139 } 22140 }; 22141 triggerSegmentEventFn({ 22142 type: 'segmenttransmuxingtiminginfoavailable', 22143 segment, 22144 timingInfo 22145 }); 22146 audioSegmentTimingInfoFn(audioSegmentTimingInfo); 22147 }, 22148 onId3: (id3Frames, dispatchType) => { 22149 id3Fn(segment, id3Frames, dispatchType); 22150 }, 22151 onCaptions: captions => { 22152 captionsFn(segment, [captions]); 22153 }, 22154 isEndOfTimeline, 22155 onEndedTimeline: () => { 22156 endedTimelineFn(); 22157 }, 22158 onTransmuxerLog, 22159 onDone: (result, error) => { 22160 if (!doneFn) { 22161 return; 22162 } 22163 22164 result.type = result.type === 'combined' ? 'video' : result.type; 22165 triggerSegmentEventFn({ 22166 type: 'segmenttransmuxingcomplete', 22167 segment 22168 }); 22169 doneFn(error, segment, result); 22170 }, 22171 segment, 22172 triggerSegmentEventFn 22173 }); // In the transmuxer, we don't yet have the ability to extract a "proper" start time. 22174 // Meaning cached frame data may corrupt our notion of where this segment 22175 // really starts. To get around this, probe for the info needed. 22176 22177 22178 workerCallback({ 22179 action: 'probeTs', 22180 transmuxer: segment.transmuxer, 22181 data: bytes, 22182 baseStartTime: segment.baseStartTime, 22183 callback: data => { 22184 segment.bytes = bytes = data.data; 22185 const probeResult = data.result; 22186 22187 if (probeResult) { 22188 trackInfoFn(segment, { 22189 hasAudio: probeResult.hasAudio, 22190 hasVideo: probeResult.hasVideo, 22191 isMuxed 22192 }); 22193 trackInfoFn = null; 22194 } 22195 22196 finish(); 22197 } 22198 }); 22199 }; 22200 22201 const handleSegmentBytes = ({ 22202 segment, 22203 bytes, 22204 trackInfoFn, 22205 timingInfoFn, 22206 videoSegmentTimingInfoFn, 22207 audioSegmentTimingInfoFn, 22208 id3Fn, 22209 captionsFn, 22210 isEndOfTimeline, 22211 endedTimelineFn, 22212 dataFn, 22213 doneFn, 22214 onTransmuxerLog, 22215 triggerSegmentEventFn 22216 }) => { 22217 let bytesAsUint8Array = new Uint8Array(bytes); // TODO: 22218 // We should have a handler that fetches the number of bytes required 22219 // to check if something is fmp4. This will allow us to save bandwidth 22220 // because we can only exclude a playlist and abort requests 22221 // by codec after trackinfo triggers. 22222 22223 if (isLikelyFmp4MediaSegment(bytesAsUint8Array)) { 22224 segment.isFmp4 = true; 22225 const { 22226 tracks 22227 } = segment.map; 22228 const isMp4TextSegment = tracks.text && (!tracks.audio || !tracks.video); 22229 22230 if (isMp4TextSegment) { 22231 dataFn(segment, { 22232 data: bytesAsUint8Array, 22233 type: 'text' 22234 }); 22235 parseMp4TextSegment(segment, tracks.text.codec, doneFn); 22236 return; 22237 } 22238 22239 const trackInfo = { 22240 isFmp4: true, 22241 hasVideo: !!tracks.video, 22242 hasAudio: !!tracks.audio 22243 }; // if we have a audio track, with a codec that is not set to 22244 // encrypted audio 22245 22246 if (tracks.audio && tracks.audio.codec && tracks.audio.codec !== 'enca') { 22247 trackInfo.audioCodec = tracks.audio.codec; 22248 } // if we have a video track, with a codec that is not set to 22249 // encrypted video 22250 22251 22252 if (tracks.video && tracks.video.codec && tracks.video.codec !== 'encv') { 22253 trackInfo.videoCodec = tracks.video.codec; 22254 } 22255 22256 if (tracks.video && tracks.audio) { 22257 trackInfo.isMuxed = true; 22258 } // since we don't support appending fmp4 data on progress, we know we have the full 22259 // segment here 22260 22261 22262 trackInfoFn(segment, trackInfo); // The probe doesn't provide the segment end time, so only callback with the start 22263 // time. The end time can be roughly calculated by the receiver using the duration. 22264 // 22265 // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as 22266 // that is the true start of the segment (where the playback engine should begin 22267 // decoding). 22268 22269 const finishLoading = (captions, id3Frames) => { 22270 // if the track still has audio at this point it is only possible 22271 // for it to be audio only. See `tracks.video && tracks.audio` if statement 22272 // above. 22273 // we make sure to use segment.bytes here as that 22274 dataFn(segment, { 22275 data: bytesAsUint8Array, 22276 type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video' 22277 }); 22278 22279 if (id3Frames && id3Frames.length) { 22280 id3Fn(segment, id3Frames); 22281 } 22282 22283 if (captions && captions.length) { 22284 captionsFn(segment, captions); 22285 } 22286 22287 doneFn(null, segment, {}); 22288 }; 22289 22290 workerCallback({ 22291 action: 'probeMp4StartTime', 22292 timescales: segment.map.timescales, 22293 data: bytesAsUint8Array, 22294 transmuxer: segment.transmuxer, 22295 callback: ({ 22296 data, 22297 startTime 22298 }) => { 22299 // transfer bytes back to us 22300 bytes = data.buffer; 22301 segment.bytes = bytesAsUint8Array = data; 22302 22303 if (trackInfo.hasAudio && !trackInfo.isMuxed) { 22304 timingInfoFn(segment, 'audio', 'start', startTime); 22305 } 22306 22307 if (trackInfo.hasVideo) { 22308 timingInfoFn(segment, 'video', 'start', startTime); 22309 } 22310 22311 workerCallback({ 22312 action: 'probeEmsgID3', 22313 data: bytesAsUint8Array, 22314 transmuxer: segment.transmuxer, 22315 offset: startTime, 22316 callback: ({ 22317 emsgData, 22318 id3Frames 22319 }) => { 22320 // transfer bytes back to us 22321 bytes = emsgData.buffer; 22322 segment.bytes = bytesAsUint8Array = emsgData; // Run through the CaptionParser in case there are captions. 22323 // Initialize CaptionParser if it hasn't been yet 22324 22325 if (!tracks.video || !emsgData.byteLength || !segment.transmuxer) { 22326 finishLoading(undefined, id3Frames); 22327 return; 22328 } 22329 22330 workerCallback({ 22331 action: 'pushMp4Captions', 22332 endAction: 'mp4Captions', 22333 transmuxer: segment.transmuxer, 22334 data: bytesAsUint8Array, 22335 timescales: segment.map.timescales, 22336 trackIds: [tracks.video.id], 22337 callback: message => { 22338 // transfer bytes back to us 22339 bytes = message.data.buffer; 22340 segment.bytes = bytesAsUint8Array = message.data; 22341 message.logs.forEach(function (log) { 22342 onTransmuxerLog(merge$1(log, { 22343 stream: 'mp4CaptionParser' 22344 })); 22345 }); 22346 finishLoading(message.captions, id3Frames); 22347 } 22348 }); 22349 } 22350 }); 22351 } 22352 }); 22353 return; 22354 } // VTT or other segments that don't need processing 22355 22356 22357 if (!segment.transmuxer) { 22358 doneFn(null, segment, {}); 22359 return; 22360 } 22361 22362 if (typeof segment.container === 'undefined') { 22363 segment.container = detectContainerForBytes(bytesAsUint8Array); 22364 } 22365 22366 if (segment.container !== 'ts' && segment.container !== 'aac') { 22367 trackInfoFn(segment, { 22368 hasAudio: false, 22369 hasVideo: false 22370 }); 22371 doneFn(null, segment, {}); 22372 return; 22373 } // ts or aac 22374 22375 22376 transmuxAndNotify({ 22377 segment, 22378 bytes, 22379 trackInfoFn, 22380 timingInfoFn, 22381 videoSegmentTimingInfoFn, 22382 audioSegmentTimingInfoFn, 22383 id3Fn, 22384 captionsFn, 22385 isEndOfTimeline, 22386 endedTimelineFn, 22387 dataFn, 22388 doneFn, 22389 onTransmuxerLog, 22390 triggerSegmentEventFn 22391 }); 22392 }; 22393 22394 const decrypt = function ({ 22395 id, 22396 key, 22397 encryptedBytes, 22398 decryptionWorker, 22399 segment, 22400 doneFn 22401 }, callback) { 22402 const decryptionHandler = event => { 22403 if (event.data.source === id) { 22404 decryptionWorker.removeEventListener('message', decryptionHandler); 22405 const decrypted = event.data.decrypted; 22406 callback(new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength)); 22407 } 22408 }; 22409 22410 decryptionWorker.onerror = () => { 22411 const message = 'An error occurred in the decryption worker'; 22412 const segmentInfo = segmentInfoPayload({ 22413 segment 22414 }); 22415 const decryptError = { 22416 message, 22417 metadata: { 22418 error: new Error(message), 22419 errorType: videojs__default["default"].Error.StreamingFailedToDecryptSegment, 22420 segmentInfo, 22421 keyInfo: { 22422 uri: segment.key.resolvedUri || segment.map.key.resolvedUri 22423 } 22424 } 22425 }; 22426 doneFn(decryptError, segment); 22427 }; 22428 22429 decryptionWorker.addEventListener('message', decryptionHandler); 22430 let keyBytes; 22431 22432 if (key.bytes.slice) { 22433 keyBytes = key.bytes.slice(); 22434 } else { 22435 keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes)); 22436 } // incrementally decrypt the bytes 22437 22438 22439 decryptionWorker.postMessage(createTransferableMessage({ 22440 source: id, 22441 encrypted: encryptedBytes, 22442 key: keyBytes, 22443 iv: key.iv 22444 }), [encryptedBytes.buffer, keyBytes.buffer]); 22445 }; 22446 /** 22447 * Decrypt the segment via the decryption web worker 22448 * 22449 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 22450 * routines 22451 * @param {Object} segment - a simplified copy of the segmentInfo object 22452 * from SegmentLoader 22453 * @param {Function} trackInfoFn - a callback that receives track info 22454 * @param {Function} timingInfoFn - a callback that receives timing info 22455 * @param {Function} videoSegmentTimingInfoFn 22456 * a callback that receives video timing info based on media times and 22457 * any adjustments made by the transmuxer 22458 * @param {Function} audioSegmentTimingInfoFn 22459 * a callback that receives audio timing info based on media times and 22460 * any adjustments made by the transmuxer 22461 * @param {boolean} isEndOfTimeline 22462 * true if this segment represents the last segment in a timeline 22463 * @param {Function} endedTimelineFn 22464 * a callback made when a timeline is ended, will only be called if 22465 * isEndOfTimeline is true 22466 * @param {Function} dataFn - a callback that is executed when segment bytes are available 22467 * and ready to use 22468 * @param {Function} doneFn - a callback that is executed after decryption has completed 22469 */ 22470 22471 22472 const decryptSegment = ({ 22473 decryptionWorker, 22474 segment, 22475 trackInfoFn, 22476 timingInfoFn, 22477 videoSegmentTimingInfoFn, 22478 audioSegmentTimingInfoFn, 22479 id3Fn, 22480 captionsFn, 22481 isEndOfTimeline, 22482 endedTimelineFn, 22483 dataFn, 22484 doneFn, 22485 onTransmuxerLog, 22486 triggerSegmentEventFn 22487 }) => { 22488 triggerSegmentEventFn({ 22489 type: 'segmentdecryptionstart' 22490 }); 22491 decrypt({ 22492 id: segment.requestId, 22493 key: segment.key, 22494 encryptedBytes: segment.encryptedBytes, 22495 decryptionWorker, 22496 segment, 22497 doneFn 22498 }, decryptedBytes => { 22499 segment.bytes = decryptedBytes; 22500 triggerSegmentEventFn({ 22501 type: 'segmentdecryptioncomplete', 22502 segment 22503 }); 22504 handleSegmentBytes({ 22505 segment, 22506 bytes: segment.bytes, 22507 trackInfoFn, 22508 timingInfoFn, 22509 videoSegmentTimingInfoFn, 22510 audioSegmentTimingInfoFn, 22511 id3Fn, 22512 captionsFn, 22513 isEndOfTimeline, 22514 endedTimelineFn, 22515 dataFn, 22516 doneFn, 22517 onTransmuxerLog, 22518 triggerSegmentEventFn 22519 }); 22520 }); 22521 }; 22522 /** 22523 * This function waits for all XHRs to finish (with either success or failure) 22524 * before continueing processing via it's callback. The function gathers errors 22525 * from each request into a single errors array so that the error status for 22526 * each request can be examined later. 22527 * 22528 * @param {Object} activeXhrs - an object that tracks all XHR requests 22529 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 22530 * routines 22531 * @param {Function} trackInfoFn - a callback that receives track info 22532 * @param {Function} timingInfoFn - a callback that receives timing info 22533 * @param {Function} videoSegmentTimingInfoFn 22534 * a callback that receives video timing info based on media times and 22535 * any adjustments made by the transmuxer 22536 * @param {Function} audioSegmentTimingInfoFn 22537 * a callback that receives audio timing info based on media times and 22538 * any adjustments made by the transmuxer 22539 * @param {Function} id3Fn - a callback that receives ID3 metadata 22540 * @param {Function} captionsFn - a callback that receives captions 22541 * @param {boolean} isEndOfTimeline 22542 * true if this segment represents the last segment in a timeline 22543 * @param {Function} endedTimelineFn 22544 * a callback made when a timeline is ended, will only be called if 22545 * isEndOfTimeline is true 22546 * @param {Function} dataFn - a callback that is executed when segment bytes are available 22547 * and ready to use 22548 * @param {Function} doneFn - a callback that is executed after all resources have been 22549 * downloaded and any decryption completed 22550 */ 22551 22552 22553 const waitForCompletion = ({ 22554 activeXhrs, 22555 decryptionWorker, 22556 trackInfoFn, 22557 timingInfoFn, 22558 videoSegmentTimingInfoFn, 22559 audioSegmentTimingInfoFn, 22560 id3Fn, 22561 captionsFn, 22562 isEndOfTimeline, 22563 endedTimelineFn, 22564 dataFn, 22565 doneFn, 22566 onTransmuxerLog, 22567 triggerSegmentEventFn 22568 }) => { 22569 let count = 0; 22570 let didError = false; 22571 return (error, segment) => { 22572 if (didError) { 22573 return; 22574 } 22575 22576 if (error) { 22577 didError = true; // If there are errors, we have to abort any outstanding requests 22578 22579 abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we 22580 // handle the aborted events from those requests, there are some cases where we may 22581 // never get an aborted event. For instance, if the network connection is lost and 22582 // there were two requests, the first may have triggered an error immediately, while 22583 // the second request remains unsent. In that case, the aborted algorithm will not 22584 // trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method 22585 // 22586 // We also can't rely on the ready state of the XHR, since the request that 22587 // triggered the connection error may also show as a ready state of 0 (unsent). 22588 // Therefore, we have to finish this group of requests immediately after the first 22589 // seen error. 22590 22591 return doneFn(error, segment); 22592 } 22593 22594 count += 1; 22595 22596 if (count === activeXhrs.length) { 22597 const segmentFinish = function () { 22598 if (segment.encryptedBytes) { 22599 return decryptSegment({ 22600 decryptionWorker, 22601 segment, 22602 trackInfoFn, 22603 timingInfoFn, 22604 videoSegmentTimingInfoFn, 22605 audioSegmentTimingInfoFn, 22606 id3Fn, 22607 captionsFn, 22608 isEndOfTimeline, 22609 endedTimelineFn, 22610 dataFn, 22611 doneFn, 22612 onTransmuxerLog, 22613 triggerSegmentEventFn 22614 }); 22615 } // Otherwise, everything is ready just continue 22616 22617 22618 handleSegmentBytes({ 22619 segment, 22620 bytes: segment.bytes, 22621 trackInfoFn, 22622 timingInfoFn, 22623 videoSegmentTimingInfoFn, 22624 audioSegmentTimingInfoFn, 22625 id3Fn, 22626 captionsFn, 22627 isEndOfTimeline, 22628 endedTimelineFn, 22629 dataFn, 22630 doneFn, 22631 onTransmuxerLog, 22632 triggerSegmentEventFn 22633 }); 22634 }; // Keep track of when *all* of the requests have completed 22635 22636 22637 segment.endOfAllRequests = Date.now(); 22638 22639 if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) { 22640 triggerSegmentEventFn({ 22641 type: 'segmentdecryptionstart', 22642 segment 22643 }); 22644 return decrypt({ 22645 decryptionWorker, 22646 // add -init to the "id" to differentiate between segment 22647 // and init segment decryption, just in case they happen 22648 // at the same time at some point in the future. 22649 id: segment.requestId + '-init', 22650 encryptedBytes: segment.map.encryptedBytes, 22651 key: segment.map.key, 22652 segment, 22653 doneFn 22654 }, decryptedBytes => { 22655 segment.map.bytes = decryptedBytes; 22656 triggerSegmentEventFn({ 22657 type: 'segmentdecryptioncomplete', 22658 segment 22659 }); 22660 parseInitSegment(segment, parseError => { 22661 if (parseError) { 22662 abortAll(activeXhrs); 22663 return doneFn(parseError, segment); 22664 } 22665 22666 segmentFinish(); 22667 }); 22668 }); 22669 } 22670 22671 segmentFinish(); 22672 } 22673 }; 22674 }; 22675 /** 22676 * Calls the abort callback if any request within the batch was aborted. Will only call 22677 * the callback once per batch of requests, even if multiple were aborted. 22678 * 22679 * @param {Object} loadendState - state to check to see if the abort function was called 22680 * @param {Function} abortFn - callback to call for abort 22681 */ 22682 22683 22684 const handleLoadEnd = ({ 22685 loadendState, 22686 abortFn 22687 }) => event => { 22688 const request = event.target; 22689 22690 if (request.aborted && abortFn && !loadendState.calledAbortFn) { 22691 abortFn(); 22692 loadendState.calledAbortFn = true; 22693 } 22694 }; 22695 /** 22696 * Simple progress event callback handler that gathers some stats before 22697 * executing a provided callback with the `segment` object 22698 * 22699 * @param {Object} segment - a simplified copy of the segmentInfo object 22700 * from SegmentLoader 22701 * @param {Function} progressFn - a callback that is executed each time a progress event 22702 * is received 22703 * @param {Function} trackInfoFn - a callback that receives track info 22704 * @param {Function} timingInfoFn - a callback that receives timing info 22705 * @param {Function} videoSegmentTimingInfoFn 22706 * a callback that receives video timing info based on media times and 22707 * any adjustments made by the transmuxer 22708 * @param {Function} audioSegmentTimingInfoFn 22709 * a callback that receives audio timing info based on media times and 22710 * any adjustments made by the transmuxer 22711 * @param {boolean} isEndOfTimeline 22712 * true if this segment represents the last segment in a timeline 22713 * @param {Function} endedTimelineFn 22714 * a callback made when a timeline is ended, will only be called if 22715 * isEndOfTimeline is true 22716 * @param {Function} dataFn - a callback that is executed when segment bytes are available 22717 * and ready to use 22718 * @param {Event} event - the progress event object from XMLHttpRequest 22719 */ 22720 22721 22722 const handleProgress = ({ 22723 segment, 22724 progressFn, 22725 trackInfoFn, 22726 timingInfoFn, 22727 videoSegmentTimingInfoFn, 22728 audioSegmentTimingInfoFn, 22729 id3Fn, 22730 captionsFn, 22731 isEndOfTimeline, 22732 endedTimelineFn, 22733 dataFn 22734 }) => event => { 22735 const request = event.target; 22736 22737 if (request.aborted) { 22738 return; 22739 } 22740 22741 segment.stats = merge$1(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data 22742 22743 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 22744 segment.stats.firstBytesReceivedAt = Date.now(); 22745 } 22746 22747 return progressFn(event, segment); 22748 }; 22749 /** 22750 * Load all resources and does any processing necessary for a media-segment 22751 * 22752 * Features: 22753 * decrypts the media-segment if it has a key uri and an iv 22754 * aborts *all* requests if *any* one request fails 22755 * 22756 * The segment object, at minimum, has the following format: 22757 * { 22758 * resolvedUri: String, 22759 * [transmuxer]: Object, 22760 * [byterange]: { 22761 * offset: Number, 22762 * length: Number 22763 * }, 22764 * [key]: { 22765 * resolvedUri: String 22766 * [byterange]: { 22767 * offset: Number, 22768 * length: Number 22769 * }, 22770 * iv: { 22771 * bytes: Uint32Array 22772 * } 22773 * }, 22774 * [map]: { 22775 * resolvedUri: String, 22776 * [byterange]: { 22777 * offset: Number, 22778 * length: Number 22779 * }, 22780 * [bytes]: Uint8Array 22781 * } 22782 * } 22783 * ...where [name] denotes optional properties 22784 * 22785 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 22786 * @param {Object} xhrOptions - the base options to provide to all xhr requests 22787 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 22788 * decryption routines 22789 * @param {Object} segment - a simplified copy of the segmentInfo object 22790 * from SegmentLoader 22791 * @param {Function} abortFn - a callback called (only once) if any piece of a request was 22792 * aborted 22793 * @param {Function} progressFn - a callback that receives progress events from the main 22794 * segment's xhr request 22795 * @param {Function} trackInfoFn - a callback that receives track info 22796 * @param {Function} timingInfoFn - a callback that receives timing info 22797 * @param {Function} videoSegmentTimingInfoFn 22798 * a callback that receives video timing info based on media times and 22799 * any adjustments made by the transmuxer 22800 * @param {Function} audioSegmentTimingInfoFn 22801 * a callback that receives audio timing info based on media times and 22802 * any adjustments made by the transmuxer 22803 * @param {Function} id3Fn - a callback that receives ID3 metadata 22804 * @param {Function} captionsFn - a callback that receives captions 22805 * @param {boolean} isEndOfTimeline 22806 * true if this segment represents the last segment in a timeline 22807 * @param {Function} endedTimelineFn 22808 * a callback made when a timeline is ended, will only be called if 22809 * isEndOfTimeline is true 22810 * @param {Function} dataFn - a callback that receives data from the main segment's xhr 22811 * request, transmuxed if needed 22812 * @param {Function} doneFn - a callback that is executed only once all requests have 22813 * succeeded or failed 22814 * @return {Function} a function that, when invoked, immediately aborts all 22815 * outstanding requests 22816 */ 22817 22818 22819 const mediaSegmentRequest = ({ 22820 xhr, 22821 xhrOptions, 22822 decryptionWorker, 22823 segment, 22824 abortFn, 22825 progressFn, 22826 trackInfoFn, 22827 timingInfoFn, 22828 videoSegmentTimingInfoFn, 22829 audioSegmentTimingInfoFn, 22830 id3Fn, 22831 captionsFn, 22832 isEndOfTimeline, 22833 endedTimelineFn, 22834 dataFn, 22835 doneFn, 22836 onTransmuxerLog, 22837 triggerSegmentEventFn 22838 }) => { 22839 const activeXhrs = []; 22840 const finishProcessingFn = waitForCompletion({ 22841 activeXhrs, 22842 decryptionWorker, 22843 trackInfoFn, 22844 timingInfoFn, 22845 videoSegmentTimingInfoFn, 22846 audioSegmentTimingInfoFn, 22847 id3Fn, 22848 captionsFn, 22849 isEndOfTimeline, 22850 endedTimelineFn, 22851 dataFn, 22852 doneFn, 22853 onTransmuxerLog, 22854 triggerSegmentEventFn 22855 }); // optionally, request the decryption key 22856 22857 if (segment.key && !segment.key.bytes) { 22858 const objects = [segment.key]; 22859 22860 if (segment.map && !segment.map.bytes && segment.map.key && segment.map.key.resolvedUri === segment.key.resolvedUri) { 22861 objects.push(segment.map.key); 22862 } 22863 22864 const keyRequestOptions = merge$1(xhrOptions, { 22865 uri: segment.key.resolvedUri, 22866 responseType: 'arraybuffer', 22867 requestType: 'segment-key' 22868 }); 22869 const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn, triggerSegmentEventFn); 22870 const keyInfo = { 22871 uri: segment.key.resolvedUri 22872 }; 22873 triggerSegmentEventFn({ 22874 type: 'segmentkeyloadstart', 22875 segment, 22876 keyInfo 22877 }); 22878 const keyXhr = xhr(keyRequestOptions, keyRequestCallback); 22879 activeXhrs.push(keyXhr); 22880 } // optionally, request the associated media init segment 22881 22882 22883 if (segment.map && !segment.map.bytes) { 22884 const differentMapKey = segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri); 22885 22886 if (differentMapKey) { 22887 const mapKeyRequestOptions = merge$1(xhrOptions, { 22888 uri: segment.map.key.resolvedUri, 22889 responseType: 'arraybuffer', 22890 requestType: 'segment-key' 22891 }); 22892 const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn, triggerSegmentEventFn); 22893 const keyInfo = { 22894 uri: segment.map.key.resolvedUri 22895 }; 22896 triggerSegmentEventFn({ 22897 type: 'segmentkeyloadstart', 22898 segment, 22899 keyInfo 22900 }); 22901 const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); 22902 activeXhrs.push(mapKeyXhr); 22903 } 22904 22905 const initSegmentOptions = merge$1(xhrOptions, { 22906 uri: segment.map.resolvedUri, 22907 responseType: 'arraybuffer', 22908 headers: segmentXhrHeaders(segment.map), 22909 requestType: 'segment-media-initialization' 22910 }); 22911 const initSegmentRequestCallback = handleInitSegmentResponse({ 22912 segment, 22913 finishProcessingFn, 22914 triggerSegmentEventFn 22915 }); 22916 triggerSegmentEventFn({ 22917 type: 'segmentloadstart', 22918 segment 22919 }); 22920 const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 22921 activeXhrs.push(initSegmentXhr); 22922 } 22923 22924 const segmentRequestOptions = merge$1(xhrOptions, { 22925 uri: segment.part && segment.part.resolvedUri || segment.resolvedUri, 22926 responseType: 'arraybuffer', 22927 headers: segmentXhrHeaders(segment), 22928 requestType: 'segment' 22929 }); 22930 const segmentRequestCallback = handleSegmentResponse({ 22931 segment, 22932 finishProcessingFn, 22933 responseType: segmentRequestOptions.responseType, 22934 triggerSegmentEventFn 22935 }); 22936 triggerSegmentEventFn({ 22937 type: 'segmentloadstart', 22938 segment 22939 }); 22940 const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 22941 segmentXhr.addEventListener('progress', handleProgress({ 22942 segment, 22943 progressFn, 22944 trackInfoFn, 22945 timingInfoFn, 22946 videoSegmentTimingInfoFn, 22947 audioSegmentTimingInfoFn, 22948 id3Fn, 22949 captionsFn, 22950 isEndOfTimeline, 22951 endedTimelineFn, 22952 dataFn 22953 })); 22954 activeXhrs.push(segmentXhr); // since all parts of the request must be considered, but should not make callbacks 22955 // multiple times, provide a shared state object 22956 22957 const loadendState = {}; 22958 activeXhrs.forEach(activeXhr => { 22959 activeXhr.addEventListener('loadend', handleLoadEnd({ 22960 loadendState, 22961 abortFn 22962 })); 22963 }); 22964 return () => abortAll(activeXhrs); 22965 }; 22966 22967 const logFn = logger('PlaylistSelector'); 22968 22969 const representationToString = function (representation) { 22970 if (!representation || !representation.playlist) { 22971 return; 22972 } 22973 22974 const playlist = representation.playlist; 22975 return JSON.stringify({ 22976 id: playlist.id, 22977 bandwidth: representation.bandwidth, 22978 width: representation.width, 22979 height: representation.height, 22980 codecs: playlist.attributes && playlist.attributes.CODECS || '' 22981 }); 22982 }; // Utilities 22983 22984 /** 22985 * Returns the CSS value for the specified property on an element 22986 * using `getComputedStyle`. Firefox has a long-standing issue where 22987 * getComputedStyle() may return null when running in an iframe with 22988 * `display: none`. 22989 * 22990 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 22991 * @param {HTMLElement} el the htmlelement to work on 22992 * @param {string} the proprety to get the style for 22993 */ 22994 22995 22996 const safeGetComputedStyle = function (el, property) { 22997 if (!el) { 22998 return ''; 22999 } 23000 23001 const result = window.getComputedStyle(el); 23002 23003 if (!result) { 23004 return ''; 23005 } 23006 23007 return result[property]; 23008 }; 23009 /** 23010 * Resuable stable sort function 23011 * 23012 * @param {Playlists} array 23013 * @param {Function} sortFn Different comparators 23014 * @function stableSort 23015 */ 23016 23017 23018 const stableSort = function (array, sortFn) { 23019 const newArray = array.slice(); 23020 array.sort(function (left, right) { 23021 const cmp = sortFn(left, right); 23022 23023 if (cmp === 0) { 23024 return newArray.indexOf(left) - newArray.indexOf(right); 23025 } 23026 23027 return cmp; 23028 }); 23029 }; 23030 /** 23031 * A comparator function to sort two playlist object by bandwidth. 23032 * 23033 * @param {Object} left a media playlist object 23034 * @param {Object} right a media playlist object 23035 * @return {number} Greater than zero if the bandwidth attribute of 23036 * left is greater than the corresponding attribute of right. Less 23037 * than zero if the bandwidth of right is greater than left and 23038 * exactly zero if the two are equal. 23039 */ 23040 23041 23042 const comparePlaylistBandwidth = function (left, right) { 23043 let leftBandwidth; 23044 let rightBandwidth; 23045 23046 if (left.attributes.BANDWIDTH) { 23047 leftBandwidth = left.attributes.BANDWIDTH; 23048 } 23049 23050 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; 23051 23052 if (right.attributes.BANDWIDTH) { 23053 rightBandwidth = right.attributes.BANDWIDTH; 23054 } 23055 23056 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; 23057 return leftBandwidth - rightBandwidth; 23058 }; 23059 /** 23060 * A comparator function to sort two playlist object by resolution (width). 23061 * 23062 * @param {Object} left a media playlist object 23063 * @param {Object} right a media playlist object 23064 * @return {number} Greater than zero if the resolution.width attribute of 23065 * left is greater than the corresponding attribute of right. Less 23066 * than zero if the resolution.width of right is greater than left and 23067 * exactly zero if the two are equal. 23068 */ 23069 23070 const comparePlaylistResolution = function (left, right) { 23071 let leftWidth; 23072 let rightWidth; 23073 23074 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { 23075 leftWidth = left.attributes.RESOLUTION.width; 23076 } 23077 23078 leftWidth = leftWidth || window.Number.MAX_VALUE; 23079 23080 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 23081 rightWidth = right.attributes.RESOLUTION.width; 23082 } 23083 23084 rightWidth = rightWidth || window.Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 23085 // have the same media dimensions/ resolution 23086 23087 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 23088 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 23089 } 23090 23091 return leftWidth - rightWidth; 23092 }; 23093 /** 23094 * Chooses the appropriate media playlist based on bandwidth and player size 23095 * 23096 * @param {Object} settings 23097 * Object of information required to use this selector 23098 * @param {Object} settings.main 23099 * Object representation of the main manifest 23100 * @param {number} settings.bandwidth 23101 * Current calculated bandwidth of the player 23102 * @param {number} settings.playerWidth 23103 * Current width of the player element (should account for the device pixel ratio) 23104 * @param {number} settings.playerHeight 23105 * Current height of the player element (should account for the device pixel ratio) 23106 * @param {number} settings.playerObjectFit 23107 * Current value of the video element's object-fit CSS property. Allows taking into 23108 * account that the video might be scaled up to cover the media element when selecting 23109 * media playlists based on player size. 23110 * @param {boolean} settings.limitRenditionByPlayerDimensions 23111 * True if the player width and height should be used during the selection, false otherwise 23112 * @param {Object} settings.playlistController 23113 * the current playlistController object 23114 * @return {Playlist} the highest bitrate playlist less than the 23115 * currently detected bandwidth, accounting for some amount of 23116 * bandwidth variance 23117 */ 23118 23119 let simpleSelector = function (settings) { 23120 const { 23121 main, 23122 bandwidth: playerBandwidth, 23123 playerWidth, 23124 playerHeight, 23125 playerObjectFit, 23126 limitRenditionByPlayerDimensions, 23127 playlistController 23128 } = settings; // If we end up getting called before `main` is available, exit early 23129 23130 if (!main) { 23131 return; 23132 } 23133 23134 const options = { 23135 bandwidth: playerBandwidth, 23136 width: playerWidth, 23137 height: playerHeight, 23138 limitRenditionByPlayerDimensions 23139 }; 23140 let playlists = main.playlists; // if playlist is audio only, select between currently active audio group playlists. 23141 23142 if (Playlist.isAudioOnly(main)) { 23143 playlists = playlistController.getAudioTrackPlaylists_(); // add audioOnly to options so that we log audioOnly: true 23144 // at the buttom of this function for debugging. 23145 23146 options.audioOnly = true; 23147 } // convert the playlists to an intermediary representation to make comparisons easier 23148 23149 23150 let sortedPlaylistReps = playlists.map(playlist => { 23151 let bandwidth; 23152 const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 23153 const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 23154 bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH; 23155 bandwidth = bandwidth || window.Number.MAX_VALUE; 23156 return { 23157 bandwidth, 23158 width, 23159 height, 23160 playlist 23161 }; 23162 }); 23163 stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); // filter out any playlists that have been excluded due to 23164 // incompatible configurations 23165 23166 sortedPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isIncompatible(rep.playlist)); // filter out any playlists that have been disabled manually through the representations 23167 // api or excluded temporarily due to playback errors. 23168 23169 let enabledPlaylistReps = sortedPlaylistReps.filter(rep => Playlist.isEnabled(rep.playlist)); 23170 23171 if (!enabledPlaylistReps.length) { 23172 // if there are no enabled playlists, then they have all been excluded or disabled 23173 // by the user through the representations api. In this case, ignore exclusion and 23174 // fallback to what the user wants by using playlists the user has not disabled. 23175 enabledPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isDisabled(rep.playlist)); 23176 } // filter out any variant that has greater effective bitrate 23177 // than the current estimated bandwidth 23178 23179 23180 const bandwidthPlaylistReps = enabledPlaylistReps.filter(rep => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth); 23181 let highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth 23182 // and then taking the very first element 23183 23184 const bandwidthBestRep = bandwidthPlaylistReps.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; // if we're not going to limit renditions by player size, make an early decision. 23185 23186 if (limitRenditionByPlayerDimensions === false) { 23187 const chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 23188 23189 if (chosenRep && chosenRep.playlist) { 23190 let type = 'sortedPlaylistReps'; 23191 23192 if (bandwidthBestRep) { 23193 type = 'bandwidthBestRep'; 23194 } 23195 23196 if (enabledPlaylistReps[0]) { 23197 type = 'enabledPlaylistReps'; 23198 } 23199 23200 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 23201 return chosenRep.playlist; 23202 } 23203 23204 logFn('could not choose a playlist with options', options); 23205 return null; 23206 } // filter out playlists without resolution information 23207 23208 23209 const haveResolution = bandwidthPlaylistReps.filter(rep => rep.width && rep.height); // sort variants by resolution 23210 23211 stableSort(haveResolution, (left, right) => left.width - right.width); // if we have the exact resolution as the player use it 23212 23213 const resolutionBestRepList = haveResolution.filter(rep => rep.width === playerWidth && rep.height === playerHeight); 23214 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution 23215 23216 const resolutionBestRep = resolutionBestRepList.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; 23217 let resolutionPlusOneList; 23218 let resolutionPlusOneSmallest; 23219 let resolutionPlusOneRep; // find the smallest variant that is larger than the player 23220 // if there is no match of exact resolution 23221 23222 if (!resolutionBestRep) { 23223 resolutionPlusOneList = haveResolution.filter(rep => { 23224 if (playerObjectFit === 'cover') { 23225 // video will be scaled up to cover the player. We need to 23226 // make sure rendition is at least as wide and as high as the 23227 // player. 23228 return rep.width > playerWidth && rep.height > playerHeight; 23229 } // video will be scaled down to fit inside the player soon as 23230 // its resolution exceeds player size in at least one dimension. 23231 23232 23233 return rep.width > playerWidth || rep.height > playerHeight; 23234 }); // find all the variants have the same smallest resolution 23235 23236 resolutionPlusOneSmallest = resolutionPlusOneList.filter(rep => rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height); // ensure that we also pick the highest bandwidth variant that 23237 // is just-larger-than the video player 23238 23239 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 23240 resolutionPlusOneRep = resolutionPlusOneSmallest.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; 23241 } 23242 23243 let leastPixelDiffRep; // If this selector proves to be better than others, 23244 // resolutionPlusOneRep and resolutionBestRep and all 23245 // the code involving them should be removed. 23246 23247 if (playlistController.leastPixelDiffSelector) { 23248 // find the variant that is closest to the player's pixel size 23249 const leastPixelDiffList = haveResolution.map(rep => { 23250 rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight); 23251 return rep; 23252 }); // get the highest bandwidth, closest resolution playlist 23253 23254 stableSort(leastPixelDiffList, (left, right) => { 23255 // sort by highest bandwidth if pixelDiff is the same 23256 if (left.pixelDiff === right.pixelDiff) { 23257 return right.bandwidth - left.bandwidth; 23258 } 23259 23260 return left.pixelDiff - right.pixelDiff; 23261 }); 23262 leastPixelDiffRep = leastPixelDiffList[0]; 23263 } // fallback chain of variants 23264 23265 23266 const chosenRep = leastPixelDiffRep || resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 23267 23268 if (chosenRep && chosenRep.playlist) { 23269 let type = 'sortedPlaylistReps'; 23270 23271 if (leastPixelDiffRep) { 23272 type = 'leastPixelDiffRep'; 23273 } else if (resolutionPlusOneRep) { 23274 type = 'resolutionPlusOneRep'; 23275 } else if (resolutionBestRep) { 23276 type = 'resolutionBestRep'; 23277 } else if (bandwidthBestRep) { 23278 type = 'bandwidthBestRep'; 23279 } else if (enabledPlaylistReps[0]) { 23280 type = 'enabledPlaylistReps'; 23281 } 23282 23283 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 23284 return chosenRep.playlist; 23285 } 23286 23287 logFn('could not choose a playlist with options', options); 23288 return null; 23289 }; 23290 23291 /** 23292 * Chooses the appropriate media playlist based on the most recent 23293 * bandwidth estimate and the player size. 23294 * 23295 * Expects to be called within the context of an instance of VhsHandler 23296 * 23297 * @return {Playlist} the highest bitrate playlist less than the 23298 * currently detected bandwidth, accounting for some amount of 23299 * bandwidth variance 23300 */ 23301 23302 const lastBandwidthSelector = function () { 23303 let pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1; 23304 23305 if (!isNaN(this.customPixelRatio)) { 23306 pixelRatio = this.customPixelRatio; 23307 } 23308 23309 return simpleSelector({ 23310 main: this.playlists.main, 23311 bandwidth: this.systemBandwidth, 23312 playerWidth: parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, 23313 playerHeight: parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, 23314 playerObjectFit: this.usePlayerObjectFit ? safeGetComputedStyle(this.tech_.el(), 'objectFit') : '', 23315 limitRenditionByPlayerDimensions: this.limitRenditionByPlayerDimensions, 23316 playlistController: this.playlistController_ 23317 }); 23318 }; 23319 /** 23320 * Chooses the appropriate media playlist based on an 23321 * exponential-weighted moving average of the bandwidth after 23322 * filtering for player size. 23323 * 23324 * Expects to be called within the context of an instance of VhsHandler 23325 * 23326 * @param {number} decay - a number between 0 and 1. Higher values of 23327 * this parameter will cause previous bandwidth estimates to lose 23328 * significance more quickly. 23329 * @return {Function} a function which can be invoked to create a new 23330 * playlist selector function. 23331 * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 23332 */ 23333 23334 const movingAverageBandwidthSelector = function (decay) { 23335 let average = -1; 23336 let lastSystemBandwidth = -1; 23337 23338 if (decay < 0 || decay > 1) { 23339 throw new Error('Moving average bandwidth decay must be between 0 and 1.'); 23340 } 23341 23342 return function () { 23343 let pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1; 23344 23345 if (!isNaN(this.customPixelRatio)) { 23346 pixelRatio = this.customPixelRatio; 23347 } 23348 23349 if (average < 0) { 23350 average = this.systemBandwidth; 23351 lastSystemBandwidth = this.systemBandwidth; 23352 } // stop the average value from decaying for every 250ms 23353 // when the systemBandwidth is constant 23354 // and 23355 // stop average from setting to a very low value when the 23356 // systemBandwidth becomes 0 in case of chunk cancellation 23357 23358 23359 if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) { 23360 average = decay * this.systemBandwidth + (1 - decay) * average; 23361 lastSystemBandwidth = this.systemBandwidth; 23362 } 23363 23364 return simpleSelector({ 23365 main: this.playlists.main, 23366 bandwidth: average, 23367 playerWidth: parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, 23368 playerHeight: parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, 23369 playerObjectFit: this.usePlayerObjectFit ? safeGetComputedStyle(this.tech_.el(), 'objectFit') : '', 23370 limitRenditionByPlayerDimensions: this.limitRenditionByPlayerDimensions, 23371 playlistController: this.playlistController_ 23372 }); 23373 }; 23374 }; 23375 /** 23376 * Chooses the appropriate media playlist based on the potential to rebuffer 23377 * 23378 * @param {Object} settings 23379 * Object of information required to use this selector 23380 * @param {Object} settings.main 23381 * Object representation of the main manifest 23382 * @param {number} settings.currentTime 23383 * The current time of the player 23384 * @param {number} settings.bandwidth 23385 * Current measured bandwidth 23386 * @param {number} settings.duration 23387 * Duration of the media 23388 * @param {number} settings.segmentDuration 23389 * Segment duration to be used in round trip time calculations 23390 * @param {number} settings.timeUntilRebuffer 23391 * Time left in seconds until the player has to rebuffer 23392 * @param {number} settings.currentTimeline 23393 * The current timeline segments are being loaded from 23394 * @param {SyncController} settings.syncController 23395 * SyncController for determining if we have a sync point for a given playlist 23396 * @return {Object|null} 23397 * {Object} return.playlist 23398 * The highest bandwidth playlist with the least amount of rebuffering 23399 * {Number} return.rebufferingImpact 23400 * The amount of time in seconds switching to this playlist will rebuffer. A 23401 * negative value means that switching will cause zero rebuffering. 23402 */ 23403 23404 const minRebufferMaxBandwidthSelector = function (settings) { 23405 const { 23406 main, 23407 currentTime, 23408 bandwidth, 23409 duration, 23410 segmentDuration, 23411 timeUntilRebuffer, 23412 currentTimeline, 23413 syncController 23414 } = settings; // filter out any playlists that have been excluded due to 23415 // incompatible configurations 23416 23417 const compatiblePlaylists = main.playlists.filter(playlist => !Playlist.isIncompatible(playlist)); // filter out any playlists that have been disabled manually through the representations 23418 // api or excluded temporarily due to playback errors. 23419 23420 let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 23421 23422 if (!enabledPlaylists.length) { 23423 // if there are no enabled playlists, then they have all been excluded or disabled 23424 // by the user through the representations api. In this case, ignore exclusion and 23425 // fallback to what the user wants by using playlists the user has not disabled. 23426 enabledPlaylists = compatiblePlaylists.filter(playlist => !Playlist.isDisabled(playlist)); 23427 } 23428 23429 const bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 23430 const rebufferingEstimates = bandwidthPlaylists.map(playlist => { 23431 const syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a 23432 // sync request first. This will double the request time 23433 23434 const numRequests = syncPoint ? 1 : 2; 23435 const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist); 23436 const rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 23437 return { 23438 playlist, 23439 rebufferingImpact 23440 }; 23441 }); 23442 const noRebufferingPlaylists = rebufferingEstimates.filter(estimate => estimate.rebufferingImpact <= 0); // Sort by bandwidth DESC 23443 23444 stableSort(noRebufferingPlaylists, (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist)); 23445 23446 if (noRebufferingPlaylists.length) { 23447 return noRebufferingPlaylists[0]; 23448 } 23449 23450 stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact); 23451 return rebufferingEstimates[0] || null; 23452 }; 23453 /** 23454 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 23455 * one with video. If no renditions with video exist, return the lowest audio rendition. 23456 * 23457 * Expects to be called within the context of an instance of VhsHandler 23458 * 23459 * @return {Object|null} 23460 * {Object} return.playlist 23461 * The lowest bitrate playlist that contains a video codec. If no such rendition 23462 * exists pick the lowest audio rendition. 23463 */ 23464 23465 const lowestBitrateCompatibleVariantSelector = function () { 23466 // filter out any playlists that have been excluded due to 23467 // incompatible configurations or playback errors 23468 const playlists = this.playlists.main.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate 23469 23470 stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); // Parse and assume that playlists with no video codec have no video 23471 // (this is not necessarily true, although it is generally true). 23472 // 23473 // If an entire manifest has no valid videos everything will get filtered 23474 // out. 23475 23476 const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.main, playlist).video); 23477 return playlistsWithVideo[0] || null; 23478 }; 23479 23480 /** 23481 * Combine all segments into a single Uint8Array 23482 * 23483 * @param {Object} segmentObj 23484 * @return {Uint8Array} concatenated bytes 23485 * @private 23486 */ 23487 const concatSegments = segmentObj => { 23488 let offset = 0; 23489 let tempBuffer; 23490 23491 if (segmentObj.bytes) { 23492 tempBuffer = new Uint8Array(segmentObj.bytes); // combine the individual segments into one large typed-array 23493 23494 segmentObj.segments.forEach(segment => { 23495 tempBuffer.set(segment, offset); 23496 offset += segment.byteLength; 23497 }); 23498 } 23499 23500 return tempBuffer; 23501 }; 23502 /** 23503 * Example: 23504 * https://host.com/path1/path2/path3/segment.ts?arg1=val1 23505 * --> 23506 * path3/segment.ts 23507 * 23508 * @param resolvedUri 23509 * @return {string} 23510 */ 23511 23512 function compactSegmentUrlDescription(resolvedUri) { 23513 try { 23514 return new URL(resolvedUri).pathname.split('/').slice(-2).join('/'); 23515 } catch (e) { 23516 return ''; 23517 } 23518 } 23519 23520 /** 23521 * @file text-tracks.js 23522 */ 23523 /** 23524 * Create captions text tracks on video.js if they do not exist 23525 * 23526 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 23527 * @param {Object} tech the video.js tech 23528 * @param {Object} captionStream the caption stream to create 23529 * @private 23530 */ 23531 23532 const createCaptionsTrackIfNotExists = function (inbandTextTracks, tech, captionStream) { 23533 if (!inbandTextTracks[captionStream]) { 23534 tech.trigger({ 23535 type: 'usage', 23536 name: 'vhs-608' 23537 }); 23538 let instreamId = captionStream; // we need to translate SERVICEn for 708 to how mux.js currently labels them 23539 23540 if (/^cc708_/.test(captionStream)) { 23541 instreamId = 'SERVICE' + captionStream.split('_')[1]; 23542 } 23543 23544 const track = tech.textTracks().getTrackById(instreamId); 23545 23546 if (track) { 23547 // Resuse an existing track with a CC# id because this was 23548 // very likely created by videojs-contrib-hls from information 23549 // in the m3u8 for us to use 23550 inbandTextTracks[captionStream] = track; 23551 } else { 23552 // This section gets called when we have caption services that aren't specified in the manifest. 23553 // Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS. 23554 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {}; 23555 let label = captionStream; 23556 let language = captionStream; 23557 let def = false; 23558 const captionService = captionServices[instreamId]; 23559 23560 if (captionService) { 23561 label = captionService.label; 23562 language = captionService.language; 23563 def = captionService.default; 23564 } // Otherwise, create a track with the default `CC#` label and 23565 // without a language 23566 23567 23568 inbandTextTracks[captionStream] = tech.addRemoteTextTrack({ 23569 kind: 'captions', 23570 id: instreamId, 23571 // TODO: investigate why this doesn't seem to turn the caption on by default 23572 default: def, 23573 label, 23574 language 23575 }, false).track; 23576 } 23577 } 23578 }; 23579 /** 23580 * Add caption text track data to a source handler given an array of captions 23581 * 23582 * @param {Object} 23583 * @param {Object} inbandTextTracks the inband text tracks 23584 * @param {number} timestampOffset the timestamp offset of the source buffer 23585 * @param {Array} captionArray an array of caption data 23586 * @private 23587 */ 23588 23589 const addCaptionData = function ({ 23590 inbandTextTracks, 23591 captionArray, 23592 timestampOffset 23593 }) { 23594 if (!captionArray) { 23595 return; 23596 } 23597 23598 const Cue = window.WebKitDataCue || window.VTTCue; 23599 captionArray.forEach(caption => { 23600 const track = caption.stream; // in CEA 608 captions, video.js/mux.js sends a content array 23601 // with positioning data 23602 23603 if (caption.content) { 23604 caption.content.forEach(value => { 23605 const cue = new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, value.text); 23606 cue.line = value.line; 23607 cue.align = 'left'; 23608 cue.position = value.position; 23609 cue.positionAlign = 'line-left'; 23610 inbandTextTracks[track].addCue(cue); 23611 }); 23612 } else { 23613 // otherwise, a text value with combined captions is sent 23614 inbandTextTracks[track].addCue(new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, caption.text)); 23615 } 23616 }); 23617 }; 23618 /** 23619 * Define properties on a cue for backwards compatability, 23620 * but warn the user that the way that they are using it 23621 * is depricated and will be removed at a later date. 23622 * 23623 * @param {Cue} cue the cue to add the properties on 23624 * @private 23625 */ 23626 23627 const deprecateOldCue = function (cue) { 23628 Object.defineProperties(cue.frame, { 23629 id: { 23630 get() { 23631 videojs__default["default"].log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 23632 return cue.value.key; 23633 } 23634 23635 }, 23636 value: { 23637 get() { 23638 videojs__default["default"].log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 23639 return cue.value.data; 23640 } 23641 23642 }, 23643 privateData: { 23644 get() { 23645 videojs__default["default"].log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 23646 return cue.value.data; 23647 } 23648 23649 } 23650 }); 23651 }; 23652 /** 23653 * Add metadata text track data to a source handler given an array of metadata 23654 * 23655 * @param {Object} 23656 * @param {Object} inbandTextTracks the inband text tracks 23657 * @param {Array} metadataArray an array of meta data 23658 * @param {number} timestampOffset the timestamp offset of the source buffer 23659 * @param {number} videoDuration the duration of the video 23660 * @private 23661 */ 23662 23663 23664 const addMetadata = ({ 23665 inbandTextTracks, 23666 metadataArray, 23667 timestampOffset, 23668 videoDuration 23669 }) => { 23670 if (!metadataArray) { 23671 return; 23672 } 23673 23674 const Cue = window.WebKitDataCue || window.VTTCue; 23675 const metadataTrack = inbandTextTracks.metadataTrack_; 23676 23677 if (!metadataTrack) { 23678 return; 23679 } 23680 23681 metadataArray.forEach(metadata => { 23682 const time = metadata.cueTime + timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN, 23683 // ignore this bit of metadata. 23684 // This likely occurs when you have an non-timed ID3 tag like TIT2, 23685 // which is the "Title/Songname/Content description" frame 23686 23687 if (typeof time !== 'number' || window.isNaN(time) || time < 0 || !(time < Infinity)) { 23688 return; 23689 } // If we have no frames, we can't create a cue. 23690 23691 23692 if (!metadata.frames || !metadata.frames.length) { 23693 return; 23694 } 23695 23696 metadata.frames.forEach(frame => { 23697 const cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 23698 cue.frame = frame; 23699 cue.value = frame; 23700 deprecateOldCue(cue); 23701 metadataTrack.addCue(cue); 23702 }); 23703 }); 23704 23705 if (!metadataTrack.cues || !metadataTrack.cues.length) { 23706 return; 23707 } // Updating the metadeta cues so that 23708 // the endTime of each cue is the startTime of the next cue 23709 // the endTime of last cue is the duration of the video 23710 23711 23712 const cues = metadataTrack.cues; 23713 const cuesArray = []; // Create a copy of the TextTrackCueList... 23714 // ...disregarding cues with a falsey value 23715 23716 for (let i = 0; i < cues.length; i++) { 23717 if (cues[i]) { 23718 cuesArray.push(cues[i]); 23719 } 23720 } // Group cues by their startTime value 23721 23722 23723 const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => { 23724 const timeSlot = obj[cue.startTime] || []; 23725 timeSlot.push(cue); 23726 obj[cue.startTime] = timeSlot; 23727 return obj; 23728 }, {}); // Sort startTimes by ascending order 23729 23730 const sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort((a, b) => Number(a) - Number(b)); // Map each cue group's endTime to the next group's startTime 23731 23732 sortedStartTimes.forEach((startTime, idx) => { 23733 const cueGroup = cuesGroupedByStartTime[startTime]; 23734 const finiteDuration = isFinite(videoDuration) ? videoDuration : startTime; 23735 const nextTime = Number(sortedStartTimes[idx + 1]) || finiteDuration; // Map each cue's endTime the next group's startTime 23736 23737 cueGroup.forEach(cue => { 23738 cue.endTime = nextTime; 23739 }); 23740 }); 23741 }; // object for mapping daterange attributes 23742 23743 const dateRangeAttr = { 23744 id: 'ID', 23745 class: 'CLASS', 23746 startDate: 'START-DATE', 23747 duration: 'DURATION', 23748 endDate: 'END-DATE', 23749 endOnNext: 'END-ON-NEXT', 23750 plannedDuration: 'PLANNED-DURATION', 23751 scte35Out: 'SCTE35-OUT', 23752 scte35In: 'SCTE35-IN' 23753 }; 23754 const dateRangeKeysToOmit = new Set(['id', 'class', 'startDate', 'duration', 'endDate', 'endOnNext', 'startTime', 'endTime', 'processDateRange']); 23755 /** 23756 * Add DateRange metadata text track to a source handler given an array of metadata 23757 * 23758 * @param {Object} 23759 * @param {Object} inbandTextTracks the inband text tracks 23760 * @param {Array} dateRanges parsed media playlist 23761 * @private 23762 */ 23763 23764 const addDateRangeMetadata = ({ 23765 inbandTextTracks, 23766 dateRanges 23767 }) => { 23768 const metadataTrack = inbandTextTracks.metadataTrack_; 23769 23770 if (!metadataTrack) { 23771 return; 23772 } 23773 23774 const Cue = window.WebKitDataCue || window.VTTCue; 23775 dateRanges.forEach(dateRange => { 23776 // we generate multiple cues for each date range with different attributes 23777 for (const key of Object.keys(dateRange)) { 23778 if (dateRangeKeysToOmit.has(key)) { 23779 continue; 23780 } 23781 23782 const cue = new Cue(dateRange.startTime, dateRange.endTime, ''); 23783 cue.id = dateRange.id; 23784 cue.type = 'com.apple.quicktime.HLS'; 23785 cue.value = { 23786 key: dateRangeAttr[key], 23787 data: dateRange[key] 23788 }; 23789 23790 if (key === 'scte35Out' || key === 'scte35In') { 23791 cue.value.data = new Uint8Array(cue.value.data.match(/[\da-f]{2}/gi)).buffer; 23792 } 23793 23794 metadataTrack.addCue(cue); 23795 } 23796 23797 dateRange.processDateRange(); 23798 }); 23799 }; 23800 /** 23801 * Create metadata text track on video.js if it does not exist 23802 * 23803 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 23804 * @param {string} dispatchType the inband metadata track dispatch type 23805 * @param {Object} tech the video.js tech 23806 * @private 23807 */ 23808 23809 const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => { 23810 if (inbandTextTracks.metadataTrack_) { 23811 return; 23812 } 23813 23814 inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack({ 23815 kind: 'metadata', 23816 label: 'Timed Metadata' 23817 }, false).track; 23818 23819 if (!videojs__default["default"].browser.IS_ANY_SAFARI) { 23820 inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType; 23821 } 23822 }; 23823 /** 23824 * Remove cues from a track on video.js. 23825 * 23826 * @param {Double} start start of where we should remove the cue 23827 * @param {Double} end end of where the we should remove the cue 23828 * @param {Object} track the text track to remove the cues from 23829 * @private 23830 */ 23831 23832 const removeCuesFromTrack = function (start, end, track) { 23833 let i; 23834 let cue; 23835 23836 if (!track) { 23837 return; 23838 } 23839 23840 if (!track.cues) { 23841 return; 23842 } 23843 23844 i = track.cues.length; 23845 23846 while (i--) { 23847 cue = track.cues[i]; // Remove any cue within the provided start and end time 23848 23849 if (cue.startTime >= start && cue.endTime <= end) { 23850 track.removeCue(cue); 23851 } 23852 } 23853 }; 23854 /** 23855 * Remove duplicate cues from a track on video.js (a cue is considered a 23856 * duplicate if it has the same time interval and text as another) 23857 * 23858 * @param {Object} track the text track to remove the duplicate cues from 23859 * @private 23860 */ 23861 23862 const removeDuplicateCuesFromTrack = function (track) { 23863 const cues = track.cues; 23864 23865 if (!cues) { 23866 return; 23867 } 23868 23869 const uniqueCues = {}; 23870 23871 for (let i = cues.length - 1; i >= 0; i--) { 23872 const cue = cues[i]; 23873 const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`; 23874 23875 if (uniqueCues[cueKey]) { 23876 track.removeCue(cue); 23877 } else { 23878 uniqueCues[cueKey] = cue; 23879 } 23880 } 23881 }; 23882 23883 /** 23884 * mux.js 23885 * 23886 * Copyright (c) Brightcove 23887 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 23888 */ 23889 var ONE_SECOND_IN_TS = 90000, 23890 // 90kHz clock 23891 secondsToVideoTs, 23892 secondsToAudioTs, 23893 videoTsToSeconds, 23894 audioTsToSeconds, 23895 audioTsToVideoTs, 23896 videoTsToAudioTs, 23897 metadataTsToSeconds; 23898 23899 secondsToVideoTs = function (seconds) { 23900 return seconds * ONE_SECOND_IN_TS; 23901 }; 23902 23903 secondsToAudioTs = function (seconds, sampleRate) { 23904 return seconds * sampleRate; 23905 }; 23906 23907 videoTsToSeconds = function (timestamp) { 23908 return timestamp / ONE_SECOND_IN_TS; 23909 }; 23910 23911 audioTsToSeconds = function (timestamp, sampleRate) { 23912 return timestamp / sampleRate; 23913 }; 23914 23915 audioTsToVideoTs = function (timestamp, sampleRate) { 23916 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 23917 }; 23918 23919 videoTsToAudioTs = function (timestamp, sampleRate) { 23920 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 23921 }; 23922 /** 23923 * Adjust ID3 tag or caption timing information by the timeline pts values 23924 * (if keepOriginalTimestamps is false) and convert to seconds 23925 */ 23926 23927 23928 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 23929 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 23930 }; 23931 23932 var clock = { 23933 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS, 23934 secondsToVideoTs: secondsToVideoTs, 23935 secondsToAudioTs: secondsToAudioTs, 23936 videoTsToSeconds: videoTsToSeconds, 23937 audioTsToSeconds: audioTsToSeconds, 23938 audioTsToVideoTs: audioTsToVideoTs, 23939 videoTsToAudioTs: videoTsToAudioTs, 23940 metadataTsToSeconds: metadataTsToSeconds 23941 }; 23942 23943 /** 23944 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 23945 * front of current time. 23946 * 23947 * @param {Array} buffer 23948 * The current buffer of gop information 23949 * @param {number} currentTime 23950 * The current time 23951 * @param {Double} mapping 23952 * Offset to map display time to stream presentation time 23953 * @return {Array} 23954 * List of gops considered safe to append over 23955 */ 23956 23957 const gopsSafeToAlignWith = (buffer, currentTime, mapping) => { 23958 if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) { 23959 return []; 23960 } // pts value for current time + 3 seconds to give a bit more wiggle room 23961 23962 23963 const currentTimePts = Math.ceil((currentTime - mapping + 3) * clock.ONE_SECOND_IN_TS); 23964 let i; 23965 23966 for (i = 0; i < buffer.length; i++) { 23967 if (buffer[i].pts > currentTimePts) { 23968 break; 23969 } 23970 } 23971 23972 return buffer.slice(i); 23973 }; 23974 /** 23975 * Appends gop information (timing and byteLength) received by the transmuxer for the 23976 * gops appended in the last call to appendBuffer 23977 * 23978 * @param {Array} buffer 23979 * The current buffer of gop information 23980 * @param {Array} gops 23981 * List of new gop information 23982 * @param {boolean} replace 23983 * If true, replace the buffer with the new gop information. If false, append the 23984 * new gop information to the buffer in the right location of time. 23985 * @return {Array} 23986 * Updated list of gop information 23987 */ 23988 23989 const updateGopBuffer = (buffer, gops, replace) => { 23990 if (!gops.length) { 23991 return buffer; 23992 } 23993 23994 if (replace) { 23995 // If we are in safe append mode, then completely overwrite the gop buffer 23996 // with the most recent appeneded data. This will make sure that when appending 23997 // future segments, we only try to align with gops that are both ahead of current 23998 // time and in the last segment appended. 23999 return gops.slice(); 24000 } 24001 24002 const start = gops[0].pts; 24003 let i = 0; 24004 24005 for (i; i < buffer.length; i++) { 24006 if (buffer[i].pts >= start) { 24007 break; 24008 } 24009 } 24010 24011 return buffer.slice(0, i).concat(gops); 24012 }; 24013 /** 24014 * Removes gop information in buffer that overlaps with provided start and end 24015 * 24016 * @param {Array} buffer 24017 * The current buffer of gop information 24018 * @param {Double} start 24019 * position to start the remove at 24020 * @param {Double} end 24021 * position to end the remove at 24022 * @param {Double} mapping 24023 * Offset to map display time to stream presentation time 24024 */ 24025 24026 const removeGopBuffer = (buffer, start, end, mapping) => { 24027 const startPts = Math.ceil((start - mapping) * clock.ONE_SECOND_IN_TS); 24028 const endPts = Math.ceil((end - mapping) * clock.ONE_SECOND_IN_TS); 24029 const updatedBuffer = buffer.slice(); 24030 let i = buffer.length; 24031 24032 while (i--) { 24033 if (buffer[i].pts <= endPts) { 24034 break; 24035 } 24036 } 24037 24038 if (i === -1) { 24039 // no removal because end of remove range is before start of buffer 24040 return updatedBuffer; 24041 } 24042 24043 let j = i + 1; 24044 24045 while (j--) { 24046 if (buffer[j].pts <= startPts) { 24047 break; 24048 } 24049 } // clamp remove range start to 0 index 24050 24051 24052 j = Math.max(j, 0); 24053 updatedBuffer.splice(j, i - j + 1); 24054 return updatedBuffer; 24055 }; 24056 24057 const shallowEqual = function (a, b) { 24058 // if both are undefined 24059 // or one or the other is undefined 24060 // they are not equal 24061 if (!a && !b || !a && b || a && !b) { 24062 return false; 24063 } // they are the same object and thus, equal 24064 24065 24066 if (a === b) { 24067 return true; 24068 } // sort keys so we can make sure they have 24069 // all the same keys later. 24070 24071 24072 const akeys = Object.keys(a).sort(); 24073 const bkeys = Object.keys(b).sort(); // different number of keys, not equal 24074 24075 if (akeys.length !== bkeys.length) { 24076 return false; 24077 } 24078 24079 for (let i = 0; i < akeys.length; i++) { 24080 const key = akeys[i]; // different sorted keys, not equal 24081 24082 if (key !== bkeys[i]) { 24083 return false; 24084 } // different values, not equal 24085 24086 24087 if (a[key] !== b[key]) { 24088 return false; 24089 } 24090 } 24091 24092 return true; 24093 }; 24094 24095 /** 24096 * The segment loader has no recourse except to fetch a segment in the 24097 * current playlist and use the internal timestamps in that segment to 24098 * generate a syncPoint. This function returns a good candidate index 24099 * for that process. 24100 * 24101 * @param {Array} segments - the segments array from a playlist. 24102 * @return {number} An index of a segment from the playlist to load 24103 */ 24104 24105 const getSyncSegmentCandidate = function (currentTimeline, segments, targetTime) { 24106 segments = segments || []; 24107 const timelineSegments = []; 24108 let time = 0; 24109 24110 for (let i = 0; i < segments.length; i++) { 24111 const segment = segments[i]; 24112 24113 if (currentTimeline === segment.timeline) { 24114 timelineSegments.push(i); 24115 time += segment.duration; 24116 24117 if (time > targetTime) { 24118 return i; 24119 } 24120 } 24121 } 24122 24123 if (timelineSegments.length === 0) { 24124 return 0; 24125 } // default to the last timeline segment 24126 24127 24128 return timelineSegments[timelineSegments.length - 1]; 24129 }; // In the event of a quota exceeded error, keep at least one second of back buffer. This 24130 // number was arbitrarily chosen and may be updated in the future, but seemed reasonable 24131 // as a start to prevent any potential issues with removing content too close to the 24132 // playhead. 24133 24134 const MIN_BACK_BUFFER = 1; // in ms 24135 24136 const CHECK_BUFFER_DELAY = 500; 24137 24138 const finite = num => typeof num === 'number' && isFinite(num); // With most content hovering around 30fps, if a segment has a duration less than a half 24139 // frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will 24140 // not accurately reflect the rest of the content. 24141 24142 24143 const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60; 24144 const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => { 24145 // Although these checks should most likely cover non 'main' types, for now it narrows 24146 // the scope of our checks. 24147 if (loaderType !== 'main' || !startingMedia || !trackInfo) { 24148 return null; 24149 } 24150 24151 if (!trackInfo.hasAudio && !trackInfo.hasVideo) { 24152 return 'Neither audio nor video found in segment.'; 24153 } 24154 24155 if (startingMedia.hasVideo && !trackInfo.hasVideo) { 24156 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.'; 24157 } 24158 24159 if (!startingMedia.hasVideo && trackInfo.hasVideo) { 24160 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.'; 24161 } 24162 24163 return null; 24164 }; 24165 /** 24166 * Calculates a time value that is safe to remove from the back buffer without interrupting 24167 * playback. 24168 * 24169 * @param {TimeRange} seekable 24170 * The current seekable range 24171 * @param {number} currentTime 24172 * The current time of the player 24173 * @param {number} targetDuration 24174 * The target duration of the current playlist 24175 * @return {number} 24176 * Time that is safe to remove from the back buffer without interrupting playback 24177 */ 24178 24179 const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => { 24180 // 30 seconds before the playhead provides a safe default for trimming. 24181 // 24182 // Choosing a reasonable default is particularly important for high bitrate content and 24183 // VOD videos/live streams with large windows, as the buffer may end up overfilled and 24184 // throw an APPEND_BUFFER_ERR. 24185 let trimTime = currentTime - Config.BACK_BUFFER_LENGTH; 24186 24187 if (seekable.length) { 24188 // Some live playlists may have a shorter window of content than the full allowed back 24189 // buffer. For these playlists, don't save content that's no longer within the window. 24190 trimTime = Math.max(trimTime, seekable.start(0)); 24191 } // Don't remove within target duration of the current time to avoid the possibility of 24192 // removing the GOP currently being played, as removing it can cause playback stalls. 24193 24194 24195 const maxTrimTime = currentTime - targetDuration; 24196 return Math.min(maxTrimTime, trimTime); 24197 }; 24198 const segmentInfoString = segmentInfo => { 24199 const { 24200 startOfSegment, 24201 duration, 24202 segment, 24203 part, 24204 playlist: { 24205 mediaSequence: seq, 24206 id, 24207 segments = [] 24208 }, 24209 mediaIndex: index, 24210 partIndex, 24211 timeline 24212 } = segmentInfo; 24213 const segmentLen = segments.length - 1; 24214 let selection = 'mediaIndex/partIndex increment'; 24215 24216 if (segmentInfo.getMediaInfoForTime) { 24217 selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`; 24218 } else if (segmentInfo.isSyncRequest) { 24219 selection = 'getSyncSegmentCandidate (isSyncRequest)'; 24220 } 24221 24222 if (segmentInfo.independent) { 24223 selection += ` with independent ${segmentInfo.independent}`; 24224 } 24225 24226 const hasPartIndex = typeof partIndex === 'number'; 24227 const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment'; 24228 const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({ 24229 preloadSegment: segment 24230 }) - 1 : 0; 24231 return `${name} [${seq + index}/${seq + segmentLen}]` + (hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') + ` segment start/end [${segment.start} => ${segment.end}]` + (hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') + ` startOfSegment [${startOfSegment}]` + ` duration [${duration}]` + ` timeline [${timeline}]` + ` selected by [${selection}]` + ` playlist [${id}]`; 24232 }; 24233 24234 const timingInfoPropertyForMedia = mediaType => `${mediaType}TimingInfo`; 24235 /** 24236 * Returns the timestamp offset to use for the segment. 24237 * 24238 * @param {number} segmentTimeline 24239 * The timeline of the segment 24240 * @param {number} currentTimeline 24241 * The timeline currently being followed by the loader 24242 * @param {number} startOfSegment 24243 * The estimated segment start 24244 * @param {TimeRange[]} buffered 24245 * The loader's buffer 24246 * @param {boolean} overrideCheck 24247 * If true, no checks are made to see if the timestamp offset value should be set, 24248 * but sets it directly to a value. 24249 * 24250 * @return {number|null} 24251 * Either a number representing a new timestamp offset, or null if the segment is 24252 * part of the same timeline 24253 */ 24254 24255 24256 const timestampOffsetForSegment = ({ 24257 segmentTimeline, 24258 currentTimeline, 24259 startOfSegment, 24260 buffered, 24261 overrideCheck 24262 }) => { 24263 // Check to see if we are crossing a discontinuity to see if we need to set the 24264 // timestamp offset on the transmuxer and source buffer. 24265 // 24266 // Previously, we changed the timestampOffset if the start of this segment was less than 24267 // the currently set timestampOffset, but this isn't desirable as it can produce bad 24268 // behavior, especially around long running live streams. 24269 if (!overrideCheck && segmentTimeline === currentTimeline) { 24270 return null; 24271 } // When changing renditions, it's possible to request a segment on an older timeline. For 24272 // instance, given two renditions with the following: 24273 // 24274 // #EXTINF:10 24275 // segment1 24276 // #EXT-X-DISCONTINUITY 24277 // #EXTINF:10 24278 // segment2 24279 // #EXTINF:10 24280 // segment3 24281 // 24282 // And the current player state: 24283 // 24284 // current time: 8 24285 // buffer: 0 => 20 24286 // 24287 // The next segment on the current rendition would be segment3, filling the buffer from 24288 // 20s onwards. However, if a rendition switch happens after segment2 was requested, 24289 // then the next segment to be requested will be segment1 from the new rendition in 24290 // order to fill time 8 and onwards. Using the buffered end would result in repeated 24291 // content (since it would position segment1 of the new rendition starting at 20s). This 24292 // case can be identified when the new segment's timeline is a prior value. Instead of 24293 // using the buffered end, the startOfSegment can be used, which, hopefully, will be 24294 // more accurate to the actual start time of the segment. 24295 24296 24297 if (segmentTimeline < currentTimeline) { 24298 return startOfSegment; 24299 } // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that 24300 // value uses the end of the last segment if it is available. While this value 24301 // should often be correct, it's better to rely on the buffered end, as the new 24302 // content post discontinuity should line up with the buffered end as if it were 24303 // time 0 for the new content. 24304 24305 24306 return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment; 24307 }; 24308 /** 24309 * Returns whether or not the loader should wait for a timeline change from the timeline 24310 * change controller before processing the segment. 24311 * 24312 * Primary timing in VHS goes by video. This is different from most media players, as 24313 * audio is more often used as the primary timing source. For the foreseeable future, VHS 24314 * will continue to use video as the primary timing source, due to the current logic and 24315 * expectations built around it. 24316 24317 * Since the timing follows video, in order to maintain sync, the video loader is 24318 * responsible for setting both audio and video source buffer timestamp offsets. 24319 * 24320 * Setting different values for audio and video source buffers could lead to 24321 * desyncing. The following examples demonstrate some of the situations where this 24322 * distinction is important. Note that all of these cases involve demuxed content. When 24323 * content is muxed, the audio and video are packaged together, therefore syncing 24324 * separate media playlists is not an issue. 24325 * 24326 * CASE 1: Audio prepares to load a new timeline before video: 24327 * 24328 * Timeline: 0 1
vendor: 2,320 bytes, lines 24329-24369
24329 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 24330 * Audio Loader: ^ 24331 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 24332 * Video Loader ^ 24333 * 24334 * In the above example, the audio loader is preparing to load the 6th segment, the first 24335 * after a discontinuity, while the video loader is still loading the 5th segment, before 24336 * the discontinuity. 24337 * 24338 * If the audio loader goes ahead and loads and appends the 6th segment before the video 24339 * loader crosses the discontinuity, then when appended, the 6th audio segment will use 24340 * the timestamp offset from timeline 0. This will likely lead to desyncing. In addition, 24341 * the audio loader must provide the audioAppendStart value to trim the content in the 24342 * transmuxer, and that value relies on the audio timestamp offset. Since the audio 24343 * timestamp offset is set by the video (main) loader, the audio loader shouldn't load the 24344 * segment until that value is provided. 24345 * 24346 * CASE 2: Video prepares to load a new timeline before audio: 24347 * 24348 * Timeline: 0 1 24349 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 24350 * Audio Loader: ^ 24351 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 24352 * Video Loader ^ 24353 * 24354 * In the above example, the video loader is preparing to load the 6th segment, the first 24355 * after a discontinuity, while the audio loader is still loading the 5th segment, before 24356 * the discontinuity. 24357 * 24358 * If the video loader goes ahead and loads and appends the 6th segment, then once the 24359 * segment is loaded and processed, both the video and audio timestamp offsets will be 24360 * set, since video is used as the primary timing source. This is to ensure content lines 24361 * up appropriately, as any modifications to the video timing are reflected by audio when 24362 * the video loader sets the audio and video timestamp offsets to the same value. However, 24363 * setting the timestamp offset for audio before audio has had a chance to change 24364 * timelines will likely lead to desyncing, as the audio loader will append segment 5 with 24365 * a timestamp intended to apply to segments from timeline 1 rather than timeline 0. 24366 * 24367 * CASE 3: When seeking, audio prepares to load a new timeline before video 24368 * 24369 * Timeline: 0 1
vendor: 201,965 bytes, lines 24370-30130
24370 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 24371 * Audio Loader: ^ 24372 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 24373 * Video Loader ^ 24374 * 24375 * In the above example, both audio and video loaders are loading segments from timeline 24376 * 0, but imagine that the seek originated from timeline 1. 24377 * 24378 * When seeking to a new timeline, the timestamp offset will be set based on the expected 24379 * segment start of the loaded video segment. In order to maintain sync, the audio loader 24380 * must wait for the video loader to load its segment and update both the audio and video 24381 * timestamp offsets before it may load and append its own segment. This is the case 24382 * whether the seek results in a mismatched segment request (e.g., the audio loader 24383 * chooses to load segment 3 and the video loader chooses to load segment 4) or the 24384 * loaders choose to load the same segment index from each playlist, as the segments may 24385 * not be aligned perfectly, even for matching segment indexes. 24386 * 24387 * @param {Object} timelinechangeController 24388 * @param {number} currentTimeline 24389 * The timeline currently being followed by the loader 24390 * @param {number} segmentTimeline 24391 * The timeline of the segment being loaded 24392 * @param {('main'|'audio')} loaderType 24393 * The loader type 24394 * @param {boolean} audioDisabled 24395 * Whether the audio is disabled for the loader. This should only be true when the 24396 * loader may have muxed audio in its segment, but should not append it, e.g., for 24397 * the main loader when an alternate audio playlist is active. 24398 * 24399 * @return {boolean} 24400 * Whether the loader should wait for a timeline change from the timeline change 24401 * controller before processing the segment 24402 */ 24403 24404 const shouldWaitForTimelineChange = ({ 24405 timelineChangeController, 24406 currentTimeline, 24407 segmentTimeline, 24408 loaderType, 24409 audioDisabled 24410 }) => { 24411 if (currentTimeline === segmentTimeline) { 24412 return false; 24413 } 24414 24415 if (loaderType === 'audio') { 24416 const lastMainTimelineChange = timelineChangeController.lastTimelineChange({ 24417 type: 'main' 24418 }); // Audio loader should wait if: 24419 // 24420 // * main hasn't had a timeline change yet (thus has not loaded its first segment) 24421 // * main hasn't yet changed to the timeline audio is looking to load 24422 24423 return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline; 24424 } // The main loader only needs to wait for timeline changes if there's demuxed audio. 24425 // Otherwise, there's nothing to wait for, since audio would be muxed into the main 24426 // loader's segments (or the content is audio/video only and handled by the main 24427 // loader). 24428 24429 24430 if (loaderType === 'main' && audioDisabled) { 24431 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({ 24432 type: 'audio' 24433 }); // Main loader should wait for the audio loader if audio is not pending a timeline 24434 // change to the current timeline. 24435 // 24436 // Since the main loader is responsible for setting the timestamp offset for both 24437 // audio and video, the main loader must wait for audio to be about to change to its 24438 // timeline before setting the offset, otherwise, if audio is behind in loading, 24439 // segments from the previous timeline would be adjusted by the new timestamp offset. 24440 // 24441 // This requirement means that video will not cross a timeline until the audio is 24442 // about to cross to it, so that way audio and video will always cross the timeline 24443 // together. 24444 // 24445 // In addition to normal timeline changes, these rules also apply to the start of a 24446 // stream (going from a non-existent timeline, -1, to timeline 0). It's important 24447 // that these rules apply to the first timeline change because if they did not, it's 24448 // possible that the main loader will cross two timelines before the audio loader has 24449 // crossed one. Logic may be implemented to handle the startup as a special case, but 24450 // it's easier to simply treat all timeline changes the same. 24451 24452 if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) { 24453 return false; 24454 } 24455 24456 return true; 24457 } 24458 24459 return false; 24460 }; 24461 const shouldFixBadTimelineChanges = timelineChangeController => { 24462 if (!timelineChangeController) { 24463 return false; 24464 } 24465 24466 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({ 24467 type: 'audio' 24468 }); 24469 const pendingMainTimelineChange = timelineChangeController.pendingTimelineChange({ 24470 type: 'main' 24471 }); 24472 const hasPendingTimelineChanges = pendingAudioTimelineChange && pendingMainTimelineChange; 24473 const differentPendingChanges = hasPendingTimelineChanges && pendingAudioTimelineChange.to !== pendingMainTimelineChange.to; 24474 const isNotInitialPendingTimelineChange = hasPendingTimelineChanges && pendingAudioTimelineChange.from !== -1 && pendingMainTimelineChange.from !== -1; 24475 24476 if (isNotInitialPendingTimelineChange && differentPendingChanges) { 24477 return true; 24478 } 24479 24480 return false; 24481 }; 24482 /** 24483 * Check if the pending audio timeline change is behind the 24484 * pending main timeline change. 24485 * 24486 * @param {SegmentLoader} segmentLoader 24487 * @return {boolean} 24488 */ 24489 24490 const isAudioTimelineBehind = segmentLoader => { 24491 const pendingAudioTimelineChange = segmentLoader.timelineChangeController_.pendingTimelineChange({ 24492 type: 'audio' 24493 }); 24494 const pendingMainTimelineChange = segmentLoader.timelineChangeController_.pendingTimelineChange({ 24495 type: 'main' 24496 }); 24497 const hasPendingTimelineChanges = pendingAudioTimelineChange && pendingMainTimelineChange; 24498 return hasPendingTimelineChanges && pendingAudioTimelineChange.to < pendingMainTimelineChange.to; 24499 }; 24500 /** 24501 * A method to check if the player is waiting for a timeline change, and fixes 24502 * certain scenarios where the timelines need to be updated. 24503 * 24504 * @param {SegmentLoader} segmentLoader 24505 */ 24506 24507 24508 const checkAndFixTimelines = segmentLoader => { 24509 const segmentInfo = segmentLoader.pendingSegment_; 24510 24511 if (!segmentInfo) { 24512 return; 24513 } 24514 24515 const waitingForTimelineChange = shouldWaitForTimelineChange({ 24516 timelineChangeController: segmentLoader.timelineChangeController_, 24517 currentTimeline: segmentLoader.currentTimeline_, 24518 segmentTimeline: segmentInfo.timeline, 24519 loaderType: segmentLoader.loaderType_, 24520 audioDisabled: segmentLoader.audioDisabled_ 24521 }); 24522 24523 if (waitingForTimelineChange && shouldFixBadTimelineChanges(segmentLoader.timelineChangeController_)) { 24524 if (isAudioTimelineBehind(segmentLoader)) { 24525 segmentLoader.timelineChangeController_.trigger('audioTimelineBehind'); 24526 return; 24527 } 24528 24529 segmentLoader.timelineChangeController_.trigger('fixBadTimelineChange'); 24530 } 24531 }; 24532 24533 const mediaDuration = timingInfos => { 24534 let maxDuration = 0; 24535 ['video', 'audio'].forEach(function (type) { 24536 const typeTimingInfo = timingInfos[`${type}TimingInfo`]; 24537 24538 if (!typeTimingInfo) { 24539 return; 24540 } 24541 24542 const { 24543 start, 24544 end 24545 } = typeTimingInfo; 24546 let duration; 24547 24548 if (typeof start === 'bigint' || typeof end === 'bigint') { 24549 duration = window.BigInt(end) - window.BigInt(start); 24550 } else if (typeof start === 'number' && typeof end === 'number') { 24551 duration = end - start; 24552 } 24553 24554 if (typeof duration !== 'undefined' && duration > maxDuration) { 24555 maxDuration = duration; 24556 } 24557 }); // convert back to a number if it is lower than MAX_SAFE_INTEGER 24558 // as we only need BigInt when we are above that. 24559 24560 if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) { 24561 maxDuration = Number(maxDuration); 24562 } 24563 24564 return maxDuration; 24565 }; 24566 const segmentTooLong = ({ 24567 segmentDuration, 24568 maxDuration 24569 }) => { 24570 // 0 duration segments are most likely due to metadata only segments or a lack of 24571 // information. 24572 if (!segmentDuration) { 24573 return false; 24574 } // For HLS: 24575 // 24576 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1 24577 // The EXTINF duration of each Media Segment in the Playlist 24578 // file, when rounded to the nearest integer, MUST be less than or equal 24579 // to the target duration; longer segments can trigger playback stalls 24580 // or other errors. 24581 // 24582 // For DASH, the mpd-parser uses the largest reported segment duration as the target 24583 // duration. Although that reported duration is occasionally approximate (i.e., not 24584 // exact), a strict check may report that a segment is too long more often in DASH. 24585 24586 24587 return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR; 24588 }; 24589 const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => { 24590 // Right now we aren't following DASH's timing model exactly, so only perform 24591 // this check for HLS content. 24592 if (sourceType !== 'hls') { 24593 return null; 24594 } 24595 24596 const segmentDuration = mediaDuration({ 24597 audioTimingInfo: segmentInfo.audioTimingInfo, 24598 videoTimingInfo: segmentInfo.videoTimingInfo 24599 }); // Don't report if we lack information. 24600 // 24601 // If the segment has a duration of 0 it is either a lack of information or a 24602 // metadata only segment and shouldn't be reported here. 24603 24604 if (!segmentDuration) { 24605 return null; 24606 } 24607 24608 const targetDuration = segmentInfo.playlist.targetDuration; 24609 const isSegmentWayTooLong = segmentTooLong({ 24610 segmentDuration, 24611 maxDuration: targetDuration * 2 24612 }); 24613 const isSegmentSlightlyTooLong = segmentTooLong({ 24614 segmentDuration, 24615 maxDuration: targetDuration 24616 }); 24617 const segmentTooLongMessage = `Segment with index ${segmentInfo.mediaIndex} ` + `from playlist ${segmentInfo.playlist.id} ` + `has a duration of ${segmentDuration} ` + `when the reported duration is ${segmentInfo.duration} ` + `and the target duration is ${targetDuration}. ` + 'For HLS content, a duration in excess of the target duration may result in ' + 'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' + 'more details: ' + 'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1'; 24618 24619 if (isSegmentWayTooLong || isSegmentSlightlyTooLong) { 24620 return { 24621 severity: isSegmentWayTooLong ? 'warn' : 'info', 24622 message: segmentTooLongMessage 24623 }; 24624 } 24625 24626 return null; 24627 }; 24628 /** 24629 * 24630 * @param {Object} options type of segment loader and segment either segmentInfo or simple segment 24631 * @return a segmentInfo payload for events or errors. 24632 */ 24633 24634 const segmentInfoPayload = ({ 24635 type, 24636 segment 24637 }) => { 24638 if (!segment) { 24639 return; 24640 } 24641 24642 const isEncrypted = Boolean(segment.key || segment.map && segment.map.ke); 24643 const isMediaInitialization = Boolean(segment.map && !segment.map.bytes); 24644 const start = segment.startOfSegment === undefined ? segment.start : segment.startOfSegment; 24645 return { 24646 type: type || segment.type, 24647 uri: segment.resolvedUri || segment.uri, 24648 start, 24649 duration: segment.duration, 24650 isEncrypted, 24651 isMediaInitialization 24652 }; 24653 }; 24654 /** 24655 * An object that manages segment loading and appending. 24656 * 24657 * @class SegmentLoader 24658 * @param {Object} options required and optional options 24659 * @extends videojs.EventTarget 24660 */ 24661 24662 class SegmentLoader extends videojs__default["default"].EventTarget { 24663 constructor(settings, options = {}) { 24664 super(); // check pre-conditions 24665 24666 if (!settings) { 24667 throw new TypeError('Initialization settings are required'); 24668 } 24669 24670 if (typeof settings.currentTime !== 'function') { 24671 throw new TypeError('No currentTime getter specified'); 24672 } 24673 24674 if (!settings.mediaSource) { 24675 throw new TypeError('No MediaSource specified'); 24676 } // public properties 24677 24678 24679 this.bandwidth = settings.bandwidth; 24680 this.throughput = { 24681 rate: 0, 24682 count: 0 24683 }; 24684 this.roundTrip = NaN; 24685 this.resetStats_(); 24686 this.mediaIndex = null; 24687 this.partIndex = null; // private settings 24688 24689 this.hasPlayed_ = settings.hasPlayed; 24690 this.currentTime_ = settings.currentTime; 24691 this.seekable_ = settings.seekable; 24692 this.seeking_ = settings.seeking; 24693 this.duration_ = settings.duration; 24694 this.mediaSource_ = settings.mediaSource; 24695 this.vhs_ = settings.vhs; 24696 this.loaderType_ = settings.loaderType; 24697 this.currentMediaInfo_ = void 0; 24698 this.startingMediaInfo_ = void 0; 24699 this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 24700 this.goalBufferLength_ = settings.goalBufferLength; 24701 this.sourceType_ = settings.sourceType; 24702 this.sourceUpdater_ = settings.sourceUpdater; 24703 this.inbandTextTracks_ = settings.inbandTextTracks; 24704 this.state_ = 'INIT'; 24705 this.timelineChangeController_ = settings.timelineChangeController; 24706 this.shouldSaveSegmentTimingInfo_ = true; 24707 this.parse708captions_ = settings.parse708captions; 24708 this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset; 24709 this.captionServices_ = settings.captionServices; 24710 this.exactManifestTimings = settings.exactManifestTimings; 24711 this.addMetadataToTextTrack = settings.addMetadataToTextTrack; // private instance variables 24712 24713 this.checkBufferTimeout_ = null; 24714 this.error_ = void 0; 24715 this.currentTimeline_ = -1; 24716 this.shouldForceTimestampOffsetAfterResync_ = false; 24717 this.pendingSegment_ = null; 24718 this.xhrOptions_ = null; 24719 this.pendingSegments_ = []; 24720 this.audioDisabled_ = false; 24721 this.isPendingTimestampOffset_ = false; // TODO possibly move gopBuffer and timeMapping info to a separate controller 24722 24723 this.gopBuffer_ = []; 24724 this.timeMapping_ = 0; 24725 this.safeAppend_ = false; 24726 this.appendInitSegment_ = { 24727 audio: true, 24728 video: true 24729 }; 24730 this.playlistOfLastInitSegment_ = { 24731 audio: null, 24732 video: null 24733 }; 24734 this.callQueue_ = []; // If the segment loader prepares to load a segment, but does not have enough 24735 // information yet to start the loading process (e.g., if the audio loader wants to 24736 // load a segment from the next timeline but the main loader hasn't yet crossed that 24737 // timeline), then the load call will be added to the queue until it is ready to be 24738 // processed. 24739 24740 this.loadQueue_ = []; 24741 this.metadataQueue_ = { 24742 id3: [], 24743 caption: [] 24744 }; 24745 this.waitingOnRemove_ = false; 24746 this.quotaExceededErrorRetryTimeout_ = null; // Fragmented mp4 playback 24747 24748 this.activeInitSegmentId_ = null; 24749 this.initSegments_ = {}; // HLSe playback 24750 24751 this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys; 24752 this.keyCache_ = {}; 24753 this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings 24754 // between a time in the display time and a segment index within 24755 // a playlist 24756 24757 this.syncController_ = settings.syncController; 24758 this.syncPoint_ = { 24759 segmentIndex: 0, 24760 time: 0 24761 }; 24762 this.transmuxer_ = this.createTransmuxer_(); 24763 24764 this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate'); 24765 24766 this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_); 24767 this.mediaSource_.addEventListener('sourceopen', () => { 24768 if (!this.isEndOfStream_()) { 24769 this.ended_ = false; 24770 } 24771 }); // ...for determining the fetch location 24772 24773 this.fetchAtBuffer_ = false; 24774 this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`); 24775 Object.defineProperty(this, 'state', { 24776 get() { 24777 return this.state_; 24778 }, 24779 24780 set(newState) { 24781 if (newState !== this.state_) { 24782 this.logger_(`${this.state_} -> ${newState}`); 24783 this.state_ = newState; 24784 this.trigger('statechange'); 24785 } 24786 } 24787 24788 }); 24789 this.sourceUpdater_.on('ready', () => { 24790 if (this.hasEnoughInfoToAppend_()) { 24791 this.processCallQueue_(); 24792 } else { 24793 checkAndFixTimelines(this); 24794 } 24795 }); 24796 this.sourceUpdater_.on('codecschange', metadata => { 24797 this.trigger(_extends({ 24798 type: 'codecschange' 24799 }, metadata)); 24800 }); // Only the main loader needs to listen for pending timeline changes, as the main 24801 // loader should wait for audio to be ready to change its timeline so that both main 24802 // and audio timelines change together. For more details, see the 24803 // shouldWaitForTimelineChange function. 24804 24805 if (this.loaderType_ === 'main') { 24806 this.timelineChangeController_.on('pendingtimelinechange', () => { 24807 if (this.hasEnoughInfoToAppend_()) { 24808 this.processCallQueue_(); 24809 } else { 24810 checkAndFixTimelines(this); 24811 } 24812 }); 24813 } // The main loader only listens on pending timeline changes, but the audio loader, 24814 // since its loads follow main, needs to listen on timeline changes. For more details, 24815 // see the shouldWaitForTimelineChange function. 24816 24817 24818 if (this.loaderType_ === 'audio') { 24819 this.timelineChangeController_.on('timelinechange', metadata => { 24820 this.trigger(_extends({ 24821 type: 'timelinechange' 24822 }, metadata)); 24823 24824 if (this.hasEnoughInfoToLoad_()) { 24825 this.processLoadQueue_(); 24826 } else { 24827 checkAndFixTimelines(this); 24828 } 24829 24830 if (this.hasEnoughInfoToAppend_()) { 24831 this.processCallQueue_(); 24832 } else { 24833 checkAndFixTimelines(this); 24834 } 24835 }); 24836 } 24837 } 24838 /** 24839 * TODO: Current sync controller consists of many hls-specific strategies 24840 * media sequence sync is also hls-specific, and we would like to be protocol-agnostic on this level 24841 * this should be a part of the sync-controller and sync controller should expect different strategy list based on the protocol. 24842 * 24843 * @return {MediaSequenceSync|null} 24844 * @private 24845 */ 24846 24847 24848 get mediaSequenceSync_() { 24849 return this.syncController_.getMediaSequenceSync(this.loaderType_); 24850 } 24851 24852 createTransmuxer_() { 24853 return segmentTransmuxer.createTransmuxer({ 24854 remux: false, 24855 alignGopsAtEnd: this.safeAppend_, 24856 keepOriginalTimestamps: true, 24857 parse708captions: this.parse708captions_, 24858 captionServices: this.captionServices_ 24859 }); 24860 } 24861 /** 24862 * reset all of our media stats 24863 * 24864 * @private 24865 */ 24866 24867 24868 resetStats_() { 24869 this.mediaBytesTransferred = 0; 24870 this.mediaRequests = 0; 24871 this.mediaRequestsAborted = 0; 24872 this.mediaRequestsTimedout = 0; 24873 this.mediaRequestsErrored = 0; 24874 this.mediaTransferDuration = 0; 24875 this.mediaSecondsLoaded = 0; 24876 this.mediaAppends = 0; 24877 } 24878 /** 24879 * dispose of the SegmentLoader and reset to the default state 24880 */ 24881 24882 24883 dispose() { 24884 this.trigger('dispose'); 24885 this.state = 'DISPOSED'; 24886 this.pause(); 24887 this.abort_(); 24888 24889 if (this.transmuxer_) { 24890 this.transmuxer_.terminate(); 24891 } 24892 24893 this.resetStats_(); 24894 24895 if (this.checkBufferTimeout_) { 24896 window.clearTimeout(this.checkBufferTimeout_); 24897 } 24898 24899 if (this.syncController_ && this.triggerSyncInfoUpdate_) { 24900 this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_); 24901 } 24902 24903 this.off(); 24904 } 24905 24906 setAudio(enable) { 24907 this.audioDisabled_ = !enable; 24908 24909 if (enable) { 24910 this.appendInitSegment_.audio = true; 24911 } else { 24912 // remove current track audio if it gets disabled 24913 this.sourceUpdater_.removeAudio(0, this.duration_()); 24914 } 24915 } 24916 /** 24917 * abort anything that is currently doing on with the SegmentLoader 24918 * and reset to a default state 24919 */ 24920 24921 24922 abort() { 24923 if (this.state !== 'WAITING') { 24924 if (this.pendingSegment_) { 24925 this.pendingSegment_ = null; 24926 } 24927 24928 this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_); 24929 return; 24930 } 24931 24932 this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY 24933 // since we are no longer "waiting" on any requests. XHR callback is not always run 24934 // when the request is aborted. This will prevent the loader from being stuck in the 24935 // WAITING state indefinitely. 24936 24937 this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the 24938 // next segment 24939 24940 if (!this.paused()) { 24941 this.monitorBuffer_(); 24942 } 24943 } 24944 /** 24945 * abort all pending xhr requests and null any pending segements 24946 * 24947 * @private 24948 */ 24949 24950 24951 abort_() { 24952 if (this.pendingSegment_ && this.pendingSegment_.abortRequests) { 24953 this.pendingSegment_.abortRequests(); 24954 } // clear out the segment being processed 24955 24956 24957 this.pendingSegment_ = null; 24958 this.callQueue_ = []; 24959 this.loadQueue_ = []; 24960 this.metadataQueue_.id3 = []; 24961 this.metadataQueue_.caption = []; 24962 this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_); 24963 this.waitingOnRemove_ = false; 24964 window.clearTimeout(this.quotaExceededErrorRetryTimeout_); 24965 this.quotaExceededErrorRetryTimeout_ = null; 24966 } 24967 24968 checkForAbort_(requestId) { 24969 // If the state is APPENDING, then aborts will not modify the state, meaning the first 24970 // callback that happens should reset the state to READY so that loading can continue. 24971 if (this.state === 'APPENDING' && !this.pendingSegment_) { 24972 this.state = 'READY'; 24973 return true; 24974 } 24975 24976 if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) { 24977 return true; 24978 } 24979 24980 return false; 24981 } 24982 /** 24983 * set an error on the segment loader and null out any pending segements 24984 * 24985 * @param {Error} error the error to set on the SegmentLoader 24986 * @return {Error} the error that was set or that is currently set 24987 */ 24988 24989 24990 error(error) { 24991 if (typeof error !== 'undefined') { 24992 this.logger_('error occurred:', error); 24993 this.error_ = error; 24994 } 24995 24996 this.pendingSegment_ = null; 24997 return this.error_; 24998 } 24999 25000 endOfStream() { 25001 this.ended_ = true; 25002 25003 if (this.transmuxer_) { 25004 // need to clear out any cached data to prepare for the new segment 25005 segmentTransmuxer.reset(this.transmuxer_); 25006 } 25007 25008 this.gopBuffer_.length = 0; 25009 this.pause(); 25010 this.trigger('ended'); 25011 } 25012 /** 25013 * Indicates which time ranges are buffered 25014 * 25015 * @return {TimeRange} 25016 * TimeRange object representing the current buffered ranges 25017 */ 25018 25019 25020 buffered_() { 25021 const trackInfo = this.getMediaInfo_(); 25022 25023 if (!this.sourceUpdater_ || !trackInfo) { 25024 return createTimeRanges(); 25025 } 25026 25027 if (this.loaderType_ === 'main') { 25028 const { 25029 hasAudio, 25030 hasVideo, 25031 isMuxed 25032 } = trackInfo; 25033 25034 if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) { 25035 return this.sourceUpdater_.buffered(); 25036 } 25037 25038 if (hasVideo) { 25039 return this.sourceUpdater_.videoBuffered(); 25040 } 25041 } // One case that can be ignored for now is audio only with alt audio, 25042 // as we don't yet have proper support for that. 25043 25044 25045 return this.sourceUpdater_.audioBuffered(); 25046 } 25047 /** 25048 * Gets and sets init segment for the provided map 25049 * 25050 * @param {Object} map 25051 * The map object representing the init segment to get or set 25052 * @param {boolean=} set 25053 * If true, the init segment for the provided map should be saved 25054 * @return {Object} 25055 * map object for desired init segment 25056 */ 25057 25058 25059 initSegmentForMap(map, set = false) { 25060 if (!map) { 25061 return null; 25062 } 25063 25064 const id = initSegmentId(map); 25065 let storedMap = this.initSegments_[id]; 25066 25067 if (set && !storedMap && map.bytes) { 25068 this.initSegments_[id] = storedMap = { 25069 resolvedUri: map.resolvedUri, 25070 byterange: map.byterange, 25071 bytes: map.bytes, 25072 tracks: map.tracks, 25073 timescales: map.timescales 25074 }; 25075 } 25076 25077 return storedMap || map; 25078 } 25079 /** 25080 * Gets and sets key for the provided key 25081 * 25082 * @param {Object} key 25083 * The key object representing the key to get or set 25084 * @param {boolean=} set 25085 * If true, the key for the provided key should be saved 25086 * @return {Object} 25087 * Key object for desired key 25088 */ 25089 25090 25091 segmentKey(key, set = false) { 25092 if (!key) { 25093 return null; 25094 } 25095 25096 const id = segmentKeyId(key); 25097 let storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per 25098 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3 25099 25100 if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) { 25101 this.keyCache_[id] = storedKey = { 25102 resolvedUri: key.resolvedUri, 25103 bytes: key.bytes 25104 }; 25105 } 25106 25107 const result = { 25108 resolvedUri: (storedKey || key).resolvedUri 25109 }; 25110 25111 if (storedKey) { 25112 result.bytes = storedKey.bytes; 25113 } 25114 25115 return result; 25116 } 25117 /** 25118 * Returns true if all configuration required for loading is present, otherwise false. 25119 * 25120 * @return {boolean} True if the all configuration is ready for loading 25121 * @private 25122 */ 25123 25124 25125 couldBeginLoading_() { 25126 return this.playlist_ && !this.paused(); 25127 } 25128 /** 25129 * load a playlist and start to fill the buffer 25130 */ 25131 25132 25133 load() { 25134 // un-pause 25135 this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be 25136 // specified 25137 25138 if (!this.playlist_) { 25139 return; 25140 } // if all the configuration is ready, initialize and begin loading 25141 25142 25143 if (this.state === 'INIT' && this.couldBeginLoading_()) { 25144 return this.init_(); 25145 } // if we're in the middle of processing a segment already, don't 25146 // kick off an additional segment request 25147 25148 25149 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') { 25150 return; 25151 } 25152 25153 this.state = 'READY'; 25154 } 25155 /** 25156 * Once all the starting parameters have been specified, begin 25157 * operation. This method should only be invoked from the INIT 25158 * state. 25159 * 25160 * @private 25161 */ 25162 25163 25164 init_() { 25165 this.state = 'READY'; // if this is the audio segment loader, and it hasn't been inited before, then any old 25166 // audio data from the muxed content should be removed 25167 25168 this.resetEverything(); 25169 return this.monitorBuffer_(); 25170 } 25171 /** 25172 * set a playlist on the segment loader 25173 * 25174 * @param {PlaylistLoader} media the playlist to set on the segment loader 25175 */ 25176 25177 25178 playlist(newPlaylist, options = {}) { 25179 if (!newPlaylist) { 25180 return; 25181 } 25182 25183 if (this.playlist_ && this.playlist_.endList && newPlaylist.endList && this.playlist_.uri === newPlaylist.uri) { 25184 // skip update if both prev and new are vod and have the same URI 25185 return; 25186 } 25187 25188 const oldPlaylist = this.playlist_; 25189 const segmentInfo = this.pendingSegment_; 25190 this.playlist_ = newPlaylist; 25191 this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist 25192 // is always our zero-time so force a sync update each time the playlist 25193 // is refreshed from the server 25194 // 25195 // Use the INIT state to determine if playback has started, as the playlist sync info 25196 // should be fixed once requests begin (as sync points are generated based on sync 25197 // info), but not before then. 25198 25199 if (this.state === 'INIT') { 25200 newPlaylist.syncInfo = { 25201 mediaSequence: newPlaylist.mediaSequence, 25202 time: 0 25203 }; // Setting the date time mapping means mapping the program date time (if available) 25204 // to time 0 on the player's timeline. The playlist's syncInfo serves a similar 25205 // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can 25206 // be updated as the playlist is refreshed before the loader starts loading, the 25207 // program date time mapping needs to be updated as well. 25208 // 25209 // This mapping is only done for the main loader because a program date time should 25210 // map equivalently between playlists. 25211 25212 if (this.loaderType_ === 'main') { 25213 this.syncController_.setDateTimeMappingForStart(newPlaylist); 25214 } 25215 } 25216 25217 let oldId = null; 25218 25219 if (oldPlaylist) { 25220 if (oldPlaylist.id) { 25221 oldId = oldPlaylist.id; 25222 } else if (oldPlaylist.uri) { 25223 oldId = oldPlaylist.uri; 25224 } 25225 } 25226 25227 this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`); 25228 25229 if (this.mediaSequenceSync_) { 25230 this.mediaSequenceSync_.update(newPlaylist, this.currentTime_()); 25231 this.logger_(`Playlist update: 25232currentTime: ${this.currentTime_()} 25233bufferedEnd: ${lastBufferedEnd(this.buffered_())} 25234`, this.mediaSequenceSync_.diagnostics); 25235 } // in VOD, this is always a rendition switch (or we updated our syncInfo above) 25236 // in LIVE, we always want to update with new playlists (including refreshes) 25237 25238 25239 this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start 25240 // buffering now 25241 25242 if (this.state === 'INIT' && this.couldBeginLoading_()) { 25243 return this.init_(); 25244 } 25245 25246 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 25247 if (this.mediaIndex !== null) { 25248 // we must reset/resync the segment loader when we switch renditions and 25249 // the segment loader is already synced to the previous rendition 25250 // We only want to reset the loader here for LLHLS playback, as resetLoader sets fetchAtBuffer_ 25251 // to false, resulting in fetching segments at currentTime and causing repeated 25252 // same-segment requests on playlist change. This erroneously drives up the playback watcher 25253 // stalled segment count, as re-requesting segments at the currentTime or browser cached segments 25254 // will not change the buffer. 25255 // Reference for LLHLS fixes: https://github.com/videojs/http-streaming/pull/1201 25256 const isLLHLS = !newPlaylist.endList && typeof newPlaylist.partTargetDuration === 'number'; 25257 25258 if (isLLHLS) { 25259 this.resetLoader(); 25260 } else { 25261 this.resyncLoader(); 25262 } 25263 } 25264 25265 this.currentMediaInfo_ = void 0; 25266 this.trigger('playlistupdate'); // the rest of this function depends on `oldPlaylist` being defined 25267 25268 return; 25269 } // we reloaded the same playlist so we are in a live scenario 25270 // and we will likely need to adjust the mediaIndex 25271 25272 25273 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 25274 this.logger_(`live window shift [${mediaSequenceDiff}]`); // update the mediaIndex on the SegmentLoader 25275 // this is important because we can abort a request and this value must be 25276 // equal to the last appended mediaIndex 25277 25278 if (this.mediaIndex !== null) { 25279 this.mediaIndex -= mediaSequenceDiff; // this can happen if we are going to load the first segment, but get a playlist 25280 // update during that. mediaIndex would go from 0 to -1 if mediaSequence in the 25281 // new playlist was incremented by 1. 25282 25283 if (this.mediaIndex < 0) { 25284 this.mediaIndex = null; 25285 this.partIndex = null; 25286 } else { 25287 const segment = this.playlist_.segments[this.mediaIndex]; // partIndex should remain the same for the same segment 25288 // unless parts fell off of the playlist for this segment. 25289 // In that case we need to reset partIndex and resync 25290 25291 if (this.partIndex && (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex])) { 25292 const mediaIndex = this.mediaIndex; 25293 this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`); 25294 this.resetLoader(); // We want to throw away the partIndex and the data associated with it, 25295 // as the part was dropped from our current playlists segment. 25296 // The mediaIndex will still be valid so keep that around. 25297 25298 this.mediaIndex = mediaIndex; 25299 } 25300 } 25301 } // update the mediaIndex on the SegmentInfo object 25302 // this is important because we will update this.mediaIndex with this value 25303 // in `handleAppendsDone_` after the segment has been successfully appended 25304 25305 25306 if (segmentInfo) { 25307 segmentInfo.mediaIndex -= mediaSequenceDiff; 25308 25309 if (segmentInfo.mediaIndex < 0) { 25310 segmentInfo.mediaIndex = null; 25311 segmentInfo.partIndex = null; 25312 } else { 25313 // we need to update the referenced segment so that timing information is 25314 // saved for the new playlist's segment, however, if the segment fell off the 25315 // playlist, we can leave the old reference and just lose the timing info 25316 if (segmentInfo.mediaIndex >= 0) { 25317 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 25318 } 25319 25320 if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) { 25321 segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex]; 25322 } 25323 } 25324 } 25325 25326 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 25327 } 25328 /** 25329 * Prevent the loader from fetching additional segments. If there 25330 * is a segment request outstanding, it will finish processing 25331 * before the loader halts. A segment loader can be unpaused by 25332 * calling load(). 25333 */ 25334 25335 25336 pause() { 25337 if (this.checkBufferTimeout_) { 25338 window.clearTimeout(this.checkBufferTimeout_); 25339 this.checkBufferTimeout_ = null; 25340 } 25341 } 25342 /** 25343 * Returns whether the segment loader is fetching additional 25344 * segments when given the opportunity. This property can be 25345 * modified through calls to pause() and load(). 25346 */ 25347 25348 25349 paused() { 25350 return this.checkBufferTimeout_ === null; 25351 } 25352 /** 25353 * Delete all the buffered data and reset the SegmentLoader 25354 * 25355 * @param {Function} [done] an optional callback to be executed when the remove 25356 * operation is complete 25357 */ 25358 25359 25360 resetEverything(done) { 25361 this.ended_ = false; 25362 this.activeInitSegmentId_ = null; 25363 this.appendInitSegment_ = { 25364 audio: true, 25365 video: true 25366 }; 25367 this.resetLoader(); // remove from 0, the earliest point, to Infinity, to signify removal of everything. 25368 // VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader, 25369 // we then clamp the value to duration if necessary. 25370 25371 this.remove(0, Infinity, done); // clears fmp4 captions 25372 25373 if (this.transmuxer_) { 25374 this.transmuxer_.postMessage({ 25375 action: 'clearAllMp4Captions' 25376 }); // reset the cache in the transmuxer 25377 25378 this.transmuxer_.postMessage({ 25379 action: 'reset' 25380 }); 25381 } 25382 } 25383 /** 25384 * Force the SegmentLoader to resync and start loading around the currentTime instead 25385 * of starting at the end of the buffer 25386 * 25387 * Useful for fast quality changes 25388 */ 25389 25390 25391 resetLoader() { 25392 this.fetchAtBuffer_ = false; 25393 25394 if (this.mediaSequenceSync_) { 25395 this.mediaSequenceSync_.resetAppendedStatus(); 25396 } 25397 25398 this.resyncLoader(); 25399 } 25400 /** 25401 * Force the SegmentLoader to restart synchronization and make a conservative guess 25402 * before returning to the simple walk-forward method 25403 */ 25404 25405 25406 resyncLoader() { 25407 if (this.transmuxer_) { 25408 // need to clear out any cached data to prepare for the new segment 25409 segmentTransmuxer.reset(this.transmuxer_); 25410 } 25411 25412 this.mediaIndex = null; 25413 this.partIndex = null; 25414 this.syncPoint_ = null; 25415 this.isPendingTimestampOffset_ = false; // this is mainly to sync timing-info when switching between renditions with and without timestamp-rollover, 25416 // so we don't want it for DASH or fragmented mp4 segments. 25417 25418 const isFmp4 = this.currentMediaInfo_ && this.currentMediaInfo_.isFmp4; 25419 const isHlsTs = this.sourceType_ === 'hls' && !isFmp4; 25420 25421 if (isHlsTs) { 25422 this.shouldForceTimestampOffsetAfterResync_ = true; 25423 } 25424 25425 this.callQueue_ = []; 25426 this.loadQueue_ = []; 25427 this.metadataQueue_.id3 = []; 25428 this.metadataQueue_.caption = []; 25429 this.abort(); 25430 25431 if (this.transmuxer_) { 25432 this.transmuxer_.postMessage({ 25433 action: 'clearParsedMp4Captions' 25434 }); 25435 } 25436 } 25437 /** 25438 * Remove any data in the source buffer between start and end times 25439 * 25440 * @param {number} start - the start time of the region to remove from the buffer 25441 * @param {number} end - the end time of the region to remove from the buffer 25442 * @param {Function} [done] - an optional callback to be executed when the remove 25443 * @param {boolean} force - force all remove operations to happen 25444 * operation is complete 25445 */ 25446 25447 25448 remove(start, end, done = () => {}, force = false) { 25449 // clamp end to duration if we need to remove everything. 25450 // This is due to a browser bug that causes issues if we remove to Infinity. 25451 // videojs/videojs-contrib-hls#1225 25452 if (end === Infinity) { 25453 end = this.duration_(); 25454 } // skip removes that would throw an error 25455 // commonly happens during a rendition switch at the start of a video 25456 // from start 0 to end 0 25457 25458 25459 if (end <= start) { 25460 this.logger_('skipping remove because end ${end} is <= start ${start}'); 25461 return; 25462 } 25463 25464 if (!this.sourceUpdater_ || !this.getMediaInfo_()) { 25465 this.logger_('skipping remove because no source updater or starting media info'); // nothing to remove if we haven't processed any media 25466 25467 return; 25468 } // set it to one to complete this function's removes 25469 25470 25471 let removesRemaining = 1; 25472 25473 const removeFinished = () => { 25474 removesRemaining--; 25475 25476 if (removesRemaining === 0) { 25477 done(); 25478 } 25479 }; 25480 25481 if (force || !this.audioDisabled_) { 25482 removesRemaining++; 25483 this.sourceUpdater_.removeAudio(start, end, removeFinished); 25484 } // While it would be better to only remove video if the main loader has video, this 25485 // should be safe with audio only as removeVideo will call back even if there's no 25486 // video buffer. 25487 // 25488 // In theory we can check to see if there's video before calling the remove, but in 25489 // the event that we're switching between renditions and from video to audio only 25490 // (when we add support for that), we may need to clear the video contents despite 25491 // what the new media will contain. 25492 25493 25494 if (force || this.loaderType_ === 'main') { 25495 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 25496 removesRemaining++; 25497 this.sourceUpdater_.removeVideo(start, end, removeFinished); 25498 } // remove any captions and ID3 tags 25499 25500 25501 for (const track in this.inbandTextTracks_) { 25502 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 25503 } 25504 25505 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); // finished this function's removes 25506 25507 removeFinished(); 25508 } 25509 /** 25510 * (re-)schedule monitorBufferTick_ to run as soon as possible 25511 * 25512 * @private 25513 */ 25514 25515 25516 monitorBuffer_() { 25517 if (this.checkBufferTimeout_) { 25518 window.clearTimeout(this.checkBufferTimeout_); 25519 } 25520 25521 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1); 25522 } 25523 /** 25524 * As long as the SegmentLoader is in the READY state, periodically 25525 * invoke fillBuffer_(). 25526 * 25527 * @private 25528 */ 25529 25530 25531 monitorBufferTick_() { 25532 if (this.state === 'READY') { 25533 this.fillBuffer_(); 25534 } 25535 25536 if (this.checkBufferTimeout_) { 25537 window.clearTimeout(this.checkBufferTimeout_); 25538 } 25539 25540 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY); 25541 } 25542 /** 25543 * fill the buffer with segements unless the sourceBuffers are 25544 * currently updating 25545 * 25546 * Note: this function should only ever be called by monitorBuffer_ 25547 * and never directly 25548 * 25549 * @private 25550 */ 25551 25552 25553 fillBuffer_() { 25554 // TODO since the source buffer maintains a queue, and we shouldn't call this function 25555 // except when we're ready for the next segment, this check can most likely be removed 25556 if (this.sourceUpdater_.updating()) { 25557 return; 25558 } // see if we need to begin loading immediately 25559 25560 25561 const segmentInfo = this.chooseNextRequest_(); 25562 25563 if (!segmentInfo) { 25564 return; 25565 } 25566 25567 const metadata = { 25568 segmentInfo: segmentInfoPayload({ 25569 type: this.loaderType_, 25570 segment: segmentInfo 25571 }) 25572 }; 25573 this.trigger({ 25574 type: 'segmentselected', 25575 metadata 25576 }); 25577 25578 if (typeof segmentInfo.timestampOffset === 'number') { 25579 this.isPendingTimestampOffset_ = false; 25580 this.timelineChangeController_.pendingTimelineChange({ 25581 type: this.loaderType_, 25582 from: this.currentTimeline_, 25583 to: segmentInfo.timeline 25584 }); 25585 } 25586 25587 this.loadSegment_(segmentInfo); 25588 } 25589 /** 25590 * Determines if we should call endOfStream on the media source based 25591 * on the state of the buffer or if appened segment was the final 25592 * segment in the playlist. 25593 * 25594 * @param {number} [mediaIndex] the media index of segment we last appended 25595 * @param {Object} [playlist] a media playlist object 25596 * @return {boolean} do we need to call endOfStream on the MediaSource 25597 */ 25598 25599 25600 isEndOfStream_(mediaIndex = this.mediaIndex, playlist = this.playlist_, partIndex = this.partIndex) { 25601 if (!playlist || !this.mediaSource_) { 25602 return false; 25603 } 25604 25605 const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex]; // mediaIndex is zero based but length is 1 based 25606 25607 const appendedLastSegment = mediaIndex + 1 === playlist.segments.length; // true if there are no parts, or this is the last part. 25608 25609 const appendedLastPart = !segment || !segment.parts || partIndex + 1 === segment.parts.length; // if we've buffered to the end of the video, we need to call endOfStream 25610 // so that MediaSources can trigger the `ended` event when it runs out of 25611 // buffered data instead of waiting for me 25612 25613 return playlist.endList && this.mediaSource_.readyState === 'open' && appendedLastSegment && appendedLastPart; 25614 } 25615 /** 25616 * Determines what request should be made given current segment loader state. 25617 * 25618 * @return {Object} a request object that describes the segment/part to load 25619 */ 25620 25621 25622 chooseNextRequest_() { 25623 const buffered = this.buffered_(); 25624 const bufferedEnd = lastBufferedEnd(buffered) || 0; 25625 const bufferedTime = timeAheadOf(buffered, this.currentTime_()); 25626 const preloaded = !this.hasPlayed_() && bufferedTime >= 1; 25627 const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_(); 25628 const segments = this.playlist_.segments; // return no segment if: 25629 // 1. we don't have segments 25630 // 2. The video has not yet played and we already downloaded a segment 25631 // 3. we already have enough buffered time 25632 25633 if (!segments.length || preloaded || haveEnoughBuffer) { 25634 return null; 25635 } 25636 25637 this.syncPoint_ = this.syncPoint_ || this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_(), this.loaderType_); 25638 const next = { 25639 partIndex: null, 25640 mediaIndex: null, 25641 startOfSegment: null, 25642 playlist: this.playlist_, 25643 isSyncRequest: Boolean(!this.syncPoint_) 25644 }; 25645 25646 if (next.isSyncRequest) { 25647 next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd); 25648 this.logger_(`choose next request. Can not find sync point. Fallback to media Index: ${next.mediaIndex}`); 25649 } else if (this.mediaIndex !== null) { 25650 const segment = segments[this.mediaIndex]; 25651 const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1; 25652 next.startOfSegment = segment.end ? segment.end : bufferedEnd; 25653 25654 if (segment.parts && segment.parts[partIndex + 1]) { 25655 next.mediaIndex = this.mediaIndex; 25656 next.partIndex = partIndex + 1; 25657 } else { 25658 next.mediaIndex = this.mediaIndex + 1; 25659 } 25660 } else { 25661 let segmentIndex; 25662 let partIndex; 25663 let startTime; 25664 const targetTime = this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(); 25665 25666 if (this.mediaSequenceSync_) { 25667 this.logger_(`chooseNextRequest_ request after Quality Switch: 25668For TargetTime: ${targetTime}. 25669CurrentTime: ${this.currentTime_()} 25670BufferedEnd: ${bufferedEnd} 25671Fetch At Buffer: ${this.fetchAtBuffer_} 25672`, this.mediaSequenceSync_.diagnostics); 25673 } 25674 25675 if (this.mediaSequenceSync_ && this.mediaSequenceSync_.isReliable) { 25676 const syncInfo = this.getSyncInfoFromMediaSequenceSync_(targetTime); 25677 25678 if (!syncInfo) { 25679 const message = 'No sync info found while using media sequence sync'; 25680 this.error({ 25681 message, 25682 metadata: { 25683 errorType: videojs__default["default"].Error.StreamingFailedToSelectNextSegment, 25684 error: new Error(message) 25685 } 25686 }); 25687 this.logger_('chooseNextRequest_ - no sync info found using media sequence sync'); // no match 25688 25689 return null; 25690 } 25691 25692 this.logger_(`chooseNextRequest_ mediaSequence syncInfo (${syncInfo.start} --> ${syncInfo.end})`); 25693 segmentIndex = syncInfo.segmentIndex; 25694 partIndex = syncInfo.partIndex; 25695 startTime = syncInfo.start; 25696 } else { 25697 this.logger_('chooseNextRequest_ - fallback to a regular segment selection algorithm, based on a syncPoint.'); // fallback 25698 25699 const mediaInfoForTime = Playlist.getMediaInfoForTime({ 25700 exactManifestTimings: this.exactManifestTimings, 25701 playlist: this.playlist_, 25702 currentTime: targetTime, 25703 startingPartIndex: this.syncPoint_.partIndex, 25704 startingSegmentIndex: this.syncPoint_.segmentIndex, 25705 startTime: this.syncPoint_.time 25706 }); 25707 segmentIndex = mediaInfoForTime.segmentIndex; 25708 partIndex = mediaInfoForTime.partIndex; 25709 startTime = mediaInfoForTime.startTime; 25710 } 25711 25712 next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${targetTime}` : `currentTime ${targetTime}`; 25713 next.mediaIndex = segmentIndex; 25714 next.startOfSegment = startTime; 25715 next.partIndex = partIndex; 25716 this.logger_(`choose next request. Playlist switched and we have a sync point. Media Index: ${next.mediaIndex} `); 25717 } 25718 25719 const nextSegment = segments[next.mediaIndex]; 25720 let nextPart = nextSegment && typeof next.partIndex === 'number' && nextSegment.parts && nextSegment.parts[next.partIndex]; // if the next segment index is invalid or 25721 // the next partIndex is invalid do not choose a next segment. 25722 25723 if (!nextSegment || typeof next.partIndex === 'number' && !nextPart) { 25724 return null; 25725 } // if the next segment has parts, and we don't have a partIndex. 25726 // Set partIndex to 0 25727 25728 25729 if (typeof next.partIndex !== 'number' && nextSegment.parts) { 25730 next.partIndex = 0; 25731 nextPart = nextSegment.parts[0]; 25732 } // independentSegments applies to every segment in a playlist. If independentSegments appears in a main playlist, 25733 // it applies to each segment in each media playlist. 25734 // https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.5.1 25735 25736 25737 const hasIndependentSegments = this.vhs_.playlists && this.vhs_.playlists.main && this.vhs_.playlists.main.independentSegments || this.playlist_.independentSegments; // if we have no buffered data then we need to make sure 25738 // that the next part we append is "independent" if possible. 25739 // So we check if the previous part is independent, and request 25740 // it if it is. 25741 25742 if (!bufferedTime && nextPart && !hasIndependentSegments && !nextPart.independent) { 25743 if (next.partIndex === 0) { 25744 const lastSegment = segments[next.mediaIndex - 1]; 25745 const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1]; 25746 25747 if (lastSegmentLastPart && lastSegmentLastPart.independent) { 25748 next.mediaIndex -= 1; 25749 next.partIndex = lastSegment.parts.length - 1; 25750 next.independent = 'previous segment'; 25751 } 25752 } else if (nextSegment.parts[next.partIndex - 1].independent) { 25753 next.partIndex -= 1; 25754 next.independent = 'previous part'; 25755 } 25756 } 25757 25758 const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended'; // do not choose a next segment if all of the following: 25759 // 1. this is the last segment in the playlist 25760 // 2. end of stream has been called on the media source already 25761 // 3. the player is not seeking 25762 25763 if (next.mediaIndex >= segments.length - 1 && ended && !this.seeking_()) { 25764 return null; 25765 } 25766 25767 if (this.shouldForceTimestampOffsetAfterResync_) { 25768 this.shouldForceTimestampOffsetAfterResync_ = false; 25769 next.forceTimestampOffset = true; 25770 this.logger_('choose next request. Force timestamp offset after loader resync'); 25771 } 25772 25773 return this.generateSegmentInfo_(next); 25774 } 25775 25776 getSyncInfoFromMediaSequenceSync_(targetTime) { 25777 if (!this.mediaSequenceSync_) { 25778 return null; 25779 } // we should pull the target time to the least available time if we drop out of sync for any reason 25780 25781 25782 const finalTargetTime = Math.max(targetTime, this.mediaSequenceSync_.start); 25783 25784 if (targetTime !== finalTargetTime) { 25785 this.logger_(`getSyncInfoFromMediaSequenceSync_. Pulled target time from ${targetTime} to ${finalTargetTime}`); 25786 } 25787 25788 const mediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(finalTargetTime); 25789 25790 if (!mediaSequenceSyncInfo) { 25791 // no match at all 25792 return null; 25793 } 25794 25795 if (!mediaSequenceSyncInfo.isAppended) { 25796 // has a perfect match 25797 return mediaSequenceSyncInfo; 25798 } // has match, but segment was already appended. 25799 // attempt to auto-advance to the nearest next segment: 25800 25801 25802 const nextMediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(mediaSequenceSyncInfo.end); 25803 25804 if (!nextMediaSequenceSyncInfo) { 25805 // no match at all 25806 return null; 25807 } 25808 25809 if (nextMediaSequenceSyncInfo.isAppended) { 25810 this.logger_('getSyncInfoFromMediaSequenceSync_: We encounter unexpected scenario where next media sequence sync info is also appended!'); 25811 } // got match with the nearest next segment 25812 25813 25814 return nextMediaSequenceSyncInfo; 25815 } 25816 25817 generateSegmentInfo_(options) { 25818 const { 25819 independent, 25820 playlist, 25821 mediaIndex, 25822 startOfSegment, 25823 isSyncRequest, 25824 partIndex, 25825 forceTimestampOffset, 25826 getMediaInfoForTime 25827 } = options; 25828 const segment = playlist.segments[mediaIndex]; 25829 const part = typeof partIndex === 'number' && segment.parts[partIndex]; 25830 const segmentInfo = { 25831 requestId: 'segment-loader-' + Math.random(), 25832 // resolve the segment URL relative to the playlist 25833 uri: part && part.resolvedUri || segment.resolvedUri, 25834 // the segment's mediaIndex at the time it was requested 25835 mediaIndex, 25836 partIndex: part ? partIndex : null, 25837 // whether or not to update the SegmentLoader's state with this 25838 // segment's mediaIndex 25839 isSyncRequest, 25840 startOfSegment, 25841 // the segment's playlist 25842 playlist, 25843 // unencrypted bytes of the segment 25844 bytes: null, 25845 // when a key is defined for this segment, the encrypted bytes 25846 encryptedBytes: null, 25847 // The target timestampOffset for this segment when we append it 25848 // to the source buffer 25849 timestampOffset: null, 25850 // The timeline that the segment is in 25851 timeline: segment.timeline, 25852 // The expected duration of the segment in seconds 25853 duration: part && part.duration || segment.duration, 25854 // retain the segment in case the playlist updates while doing an async process 25855 segment, 25856 part, 25857 byteLength: 0, 25858 transmuxer: this.transmuxer_, 25859 // type of getMediaInfoForTime that was used to get this segment 25860 getMediaInfoForTime, 25861 independent 25862 }; 25863 const overrideCheck = typeof forceTimestampOffset !== 'undefined' ? forceTimestampOffset : this.isPendingTimestampOffset_; 25864 segmentInfo.timestampOffset = this.timestampOffsetForSegment_({ 25865 segmentTimeline: segment.timeline, 25866 currentTimeline: this.currentTimeline_, 25867 startOfSegment, 25868 buffered: this.buffered_(), 25869 overrideCheck 25870 }); 25871 const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered()); 25872 25873 if (typeof audioBufferedEnd === 'number') { 25874 // since the transmuxer is using the actual timing values, but the buffer is 25875 // adjusted by the timestamp offset, we must adjust the value here 25876 segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset(); 25877 } 25878 25879 if (this.sourceUpdater_.videoBuffered().length) { 25880 segmentInfo.gopsToAlignWith = gopsSafeToAlignWith(this.gopBuffer_, // since the transmuxer is using the actual timing values, but the time is 25881 // adjusted by the timestmap offset, we must adjust the value here 25882 this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(), this.timeMapping_); 25883 } 25884 25885 return segmentInfo; 25886 } // get the timestampoffset for a segment, 25887 // added so that vtt segment loader can override and prevent 25888 // adding timestamp offsets. 25889 25890 25891 timestampOffsetForSegment_(options) { 25892 return timestampOffsetForSegment(options); 25893 } 25894 /** 25895 * Determines if the network has enough bandwidth to complete the current segment 25896 * request in a timely manner. If not, the request will be aborted early and bandwidth 25897 * updated to trigger a playlist switch. 25898 * 25899 * @param {Object} stats 25900 * Object containing stats about the request timing and size 25901 * @private 25902 */ 25903 25904 25905 earlyAbortWhenNeeded_(stats) { 25906 if (this.vhs_.tech_.paused() || // Don't abort if the current playlist is on the lowestEnabledRendition 25907 // TODO: Replace using timeout with a boolean indicating whether this playlist is 25908 // the lowestEnabledRendition. 25909 !this.xhrOptions_.timeout || // Don't abort if we have no bandwidth information to estimate segment sizes 25910 !this.playlist_.attributes.BANDWIDTH) { 25911 return; 25912 } // Wait at least 1 second since the first byte of data has been received before 25913 // using the calculated bandwidth from the progress event to allow the bitrate 25914 // to stabilize 25915 25916 25917 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 25918 return; 25919 } 25920 25921 const currentTime = this.currentTime_(); 25922 const measuredBandwidth = stats.bandwidth; 25923 const segmentDuration = this.pendingSegment_.duration; 25924 const requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 25925 // if we are only left with less than 1 second when the request completes. 25926 // A negative timeUntilRebuffering indicates we are already rebuffering 25927 25928 const timeUntilRebuffer$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.vhs_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download 25929 // is larger than the estimated time until the player runs out of forward buffer 25930 25931 if (requestTimeRemaining <= timeUntilRebuffer$1) { 25932 return; 25933 } 25934 25935 const switchCandidate = minRebufferMaxBandwidthSelector({ 25936 main: this.vhs_.playlists.main, 25937 currentTime, 25938 bandwidth: measuredBandwidth, 25939 duration: this.duration_(), 25940 segmentDuration, 25941 timeUntilRebuffer: timeUntilRebuffer$1, 25942 currentTimeline: this.currentTimeline_, 25943 syncController: this.syncController_ 25944 }); 25945 25946 if (!switchCandidate) { 25947 return; 25948 } 25949 25950 const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$1; 25951 const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 25952 let minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the 25953 // potential round trip time of the new request so that we are not too aggressive 25954 // with switching to a playlist that might save us a fraction of a second. 25955 25956 if (timeUntilRebuffer$1 <= TIME_FUDGE_FACTOR) { 25957 minimumTimeSaving = 1; 25958 } 25959 25960 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) { 25961 return; 25962 } // set the bandwidth to that of the desired playlist being sure to scale by 25963 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 25964 // don't trigger a bandwidthupdate as the bandwidth is artifial 25965 25966 25967 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 25968 this.trigger('earlyabort'); 25969 } 25970 25971 handleAbort_(segmentInfo) { 25972 this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`); 25973 this.mediaRequestsAborted += 1; 25974 } 25975 /** 25976 * XHR `progress` event handler 25977 * 25978 * @param {Event} 25979 * The XHR `progress` event 25980 * @param {Object} simpleSegment 25981 * A simplified segment object copy 25982 * @private 25983 */ 25984 25985 25986 handleProgress_(event, simpleSegment) { 25987 this.earlyAbortWhenNeeded_(simpleSegment.stats); 25988 25989 if (this.checkForAbort_(simpleSegment.requestId)) { 25990 return; 25991 } 25992 25993 this.trigger('progress'); 25994 } 25995 25996 handleTrackInfo_(simpleSegment, trackInfo) { 25997 const { 25998 hasAudio, 25999 hasVideo 26000 } = trackInfo; 26001 const metadata = { 26002 segmentInfo: segmentInfoPayload({ 26003 type: this.loaderType_, 26004 segment: simpleSegment 26005 }), 26006 trackInfo: { 26007 hasAudio, 26008 hasVideo 26009 } 26010 }; 26011 this.trigger({ 26012 type: 'segmenttransmuxingtrackinfoavailable', 26013 metadata 26014 }); 26015 this.earlyAbortWhenNeeded_(simpleSegment.stats); 26016 26017 if (this.checkForAbort_(simpleSegment.requestId)) { 26018 return; 26019 } 26020 26021 if (this.checkForIllegalMediaSwitch(trackInfo)) { 26022 return; 26023 } 26024 26025 trackInfo = trackInfo || {}; // When we have track info, determine what media types this loader is dealing with. 26026 // Guard against cases where we're not getting track info at all until we are 26027 // certain that all streams will provide it. 26028 26029 if (!shallowEqual(this.currentMediaInfo_, trackInfo)) { 26030 this.appendInitSegment_ = { 26031 audio: true, 26032 video: true 26033 }; 26034 this.startingMediaInfo_ = trackInfo; 26035 this.currentMediaInfo_ = trackInfo; 26036 this.logger_('trackinfo update', trackInfo); 26037 this.trigger('trackinfo'); 26038 } // trackinfo may cause an abort if the trackinfo 26039 // causes a codec change to an unsupported codec. 26040 26041 26042 if (this.checkForAbort_(simpleSegment.requestId)) { 26043 return; 26044 } // set trackinfo on the pending segment so that 26045 // it can append. 26046 26047 26048 this.pendingSegment_.trackInfo = trackInfo; // check if any calls were waiting on the track info 26049 26050 if (this.hasEnoughInfoToAppend_()) { 26051 this.processCallQueue_(); 26052 } else { 26053 checkAndFixTimelines(this); 26054 } 26055 } 26056 26057 handleTimingInfo_(simpleSegment, mediaType, timeType, time) { 26058 this.earlyAbortWhenNeeded_(simpleSegment.stats); 26059 26060 if (this.checkForAbort_(simpleSegment.requestId)) { 26061 return; 26062 } 26063 26064 const segmentInfo = this.pendingSegment_; 26065 const timingInfoProperty = timingInfoPropertyForMedia(mediaType); 26066 segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {}; 26067 segmentInfo[timingInfoProperty][timeType] = time; 26068 this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`); // check if any calls were waiting on the timing info 26069 26070 if (this.hasEnoughInfoToAppend_()) { 26071 this.processCallQueue_(); 26072 } else { 26073 checkAndFixTimelines(this); 26074 } 26075 } 26076 26077 handleCaptions_(simpleSegment, captionData) { 26078 this.earlyAbortWhenNeeded_(simpleSegment.stats); 26079 26080 if (this.checkForAbort_(simpleSegment.requestId)) { 26081 return; 26082 } // This could only happen with fmp4 segments, but 26083 // should still not happen in general 26084 26085 26086 if (captionData.length === 0) { 26087 this.logger_('SegmentLoader received no captions from a caption event'); 26088 return; 26089 } 26090 26091 const segmentInfo = this.pendingSegment_; // Wait until we have some video data so that caption timing 26092 // can be adjusted by the timestamp offset 26093 26094 if (!segmentInfo.hasAppendedData_) { 26095 this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData)); 26096 return; 26097 } 26098 26099 const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset(); 26100 const captionTracks = {}; // get total start/end and captions for each track/stream 26101 26102 captionData.forEach(caption => { 26103 // caption.stream is actually a track name... 26104 // set to the existing values in tracks or default values 26105 captionTracks[caption.stream] = captionTracks[caption.stream] || { 26106 // Infinity, as any other value will be less than this 26107 startTime: Infinity, 26108 captions: [], 26109 // 0 as an other value will be more than this 26110 endTime: 0 26111 }; 26112 const captionTrack = captionTracks[caption.stream]; 26113 captionTrack.startTime = Math.min(captionTrack.startTime, caption.startTime + timestampOffset); 26114 captionTrack.endTime = Math.max(captionTrack.endTime, caption.endTime + timestampOffset); 26115 captionTrack.captions.push(caption); 26116 }); 26117 Object.keys(captionTracks).forEach(trackName => { 26118 const { 26119 startTime, 26120 endTime, 26121 captions 26122 } = captionTracks[trackName]; 26123 const inbandTextTracks = this.inbandTextTracks_; 26124 this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`); 26125 createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName); // clear out any cues that start and end at the same time period for the same track. 26126 // We do this because a rendition change that also changes the timescale for captions 26127 // will result in captions being re-parsed for certain segments. If we add them again 26128 // without clearing we will have two of the same captions visible. 26129 26130 removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]); 26131 addCaptionData({ 26132 captionArray: captions, 26133 inbandTextTracks, 26134 timestampOffset 26135 }); 26136 }); // Reset stored captions since we added parsed 26137 // captions to a text track at this point 26138 26139 if (this.transmuxer_) { 26140 this.transmuxer_.postMessage({ 26141 action: 'clearParsedMp4Captions' 26142 }); 26143 } 26144 } 26145 26146 handleId3_(simpleSegment, id3Frames, dispatchType) { 26147 this.earlyAbortWhenNeeded_(simpleSegment.stats); 26148 26149 if (this.checkForAbort_(simpleSegment.requestId)) { 26150 return; 26151 } 26152 26153 const segmentInfo = this.pendingSegment_; // we need to have appended data in order for the timestamp offset to be set 26154 26155 if (!segmentInfo.hasAppendedData_) { 26156 this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType)); 26157 return; 26158 } 26159 26160 this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_()); 26161 } 26162 26163 processMetadataQueue_() { 26164 this.metadataQueue_.id3.forEach(fn => fn()); 26165 this.metadataQueue_.caption.forEach(fn => fn()); 26166 this.metadataQueue_.id3 = []; 26167 this.metadataQueue_.caption = []; 26168 } 26169 26170 processCallQueue_() { 26171 const callQueue = this.callQueue_; // Clear out the queue before the queued functions are run, since some of the 26172 // functions may check the length of the load queue and default to pushing themselves 26173 // back onto the queue. 26174 26175 this.callQueue_ = []; 26176 callQueue.forEach(fun => fun()); 26177 } 26178 26179 processLoadQueue_() { 26180 const loadQueue = this.loadQueue_; // Clear out the queue before the queued functions are run, since some of the 26181 // functions may check the length of the load queue and default to pushing themselves 26182 // back onto the queue. 26183 26184 this.loadQueue_ = []; 26185 loadQueue.forEach(fun => fun()); 26186 } 26187 /** 26188 * Determines whether the loader has enough info to load the next segment. 26189 * 26190 * @return {boolean} 26191 * Whether or not the loader has enough info to load the next segment 26192 */ 26193 26194 26195 hasEnoughInfoToLoad_() { 26196 // Since primary timing goes by video, only the audio loader potentially needs to wait 26197 // to load. 26198 if (this.loaderType_ !== 'audio') { 26199 return true; 26200 } 26201 26202 const segmentInfo = this.pendingSegment_; // A fill buffer must have already run to establish a pending segment before there's 26203 // enough info to load. 26204 26205 if (!segmentInfo) { 26206 return false; 26207 } // The first segment can and should be loaded immediately so that source buffers are 26208 // created together (before appending). Source buffer creation uses the presence of 26209 // audio and video data to determine whether to create audio/video source buffers, and 26210 // uses processed (transmuxed or parsed) media to determine the types required. 26211 26212 26213 if (!this.getCurrentMediaInfo_()) { 26214 return true; 26215 } 26216 26217 if ( // Technically, instead of waiting to load a segment on timeline changes, a segment 26218 // can be requested and downloaded and only wait before it is transmuxed or parsed. 26219 // But in practice, there are a few reasons why it is better to wait until a loader 26220 // is ready to append that segment before requesting and downloading: 26221 // 26222 // 1. Because audio and main loaders cross discontinuities together, if this loader 26223 // is waiting for the other to catch up, then instead of requesting another 26224 // segment and using up more bandwidth, by not yet loading, more bandwidth is 26225 // allotted to the loader currently behind. 26226 // 2. media-segment-request doesn't have to have logic to consider whether a segment 26227 // is ready to be processed or not, isolating the queueing behavior to the loader. 26228 // 3. The audio loader bases some of its segment properties on timing information 26229 // provided by the main loader, meaning that, if the logic for waiting on 26230 // processing was in media-segment-request, then it would also need to know how 26231 // to re-generate the segment information after the main loader caught up. 26232 shouldWaitForTimelineChange({ 26233 timelineChangeController: this.timelineChangeController_, 26234 currentTimeline: this.currentTimeline_, 26235 segmentTimeline: segmentInfo.timeline, 26236 loaderType: this.loaderType_, 26237 audioDisabled: this.audioDisabled_ 26238 })) { 26239 return false; 26240 } 26241 26242 return true; 26243 } 26244 26245 getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) { 26246 return segmentInfo && segmentInfo.trackInfo || this.currentMediaInfo_; 26247 } 26248 26249 getMediaInfo_(segmentInfo = this.pendingSegment_) { 26250 return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_; 26251 } 26252 26253 getPendingSegmentPlaylist() { 26254 return this.pendingSegment_ ? this.pendingSegment_.playlist : null; 26255 } 26256 26257 hasEnoughInfoToAppend_() { 26258 if (!this.sourceUpdater_.ready()) { 26259 return false; 26260 } // If content needs to be removed or the loader is waiting on an append reattempt, 26261 // then no additional content should be appended until the prior append is resolved. 26262 26263 26264 if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) { 26265 return false; 26266 } 26267 26268 const segmentInfo = this.pendingSegment_; 26269 const trackInfo = this.getCurrentMediaInfo_(); // no segment to append any data for or 26270 // we do not have information on this specific 26271 // segment yet 26272 26273 if (!segmentInfo || !trackInfo) { 26274 return false; 26275 } 26276 26277 const { 26278 hasAudio, 26279 hasVideo, 26280 isMuxed 26281 } = trackInfo; 26282 26283 if (hasVideo && !segmentInfo.videoTimingInfo) { 26284 return false; 26285 } // muxed content only relies on video timing information for now. 26286 26287 26288 if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) { 26289 return false; 26290 } // we need to allow an append here even if we're moving to different timelines. 26291 26292 26293 if (shouldWaitForTimelineChange({ 26294 timelineChangeController: this.timelineChangeController_, 26295 currentTimeline: this.currentTimeline_, 26296 segmentTimeline: segmentInfo.timeline, 26297 loaderType: this.loaderType_, 26298 audioDisabled: this.audioDisabled_ 26299 })) { 26300 return false; 26301 } 26302 26303 return true; 26304 } 26305 26306 handleData_(simpleSegment, result) { 26307 this.earlyAbortWhenNeeded_(simpleSegment.stats); 26308 26309 if (this.checkForAbort_(simpleSegment.requestId)) { 26310 return; 26311 } // If there's anything in the call queue, then this data came later and should be 26312 // executed after the calls currently queued. 26313 26314 26315 if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) { 26316 checkAndFixTimelines(this); 26317 this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result)); 26318 return; 26319 } 26320 26321 const segmentInfo = this.pendingSegment_; // update the time mapping so we can translate from display time to media time 26322 26323 this.setTimeMapping_(segmentInfo.timeline); // for tracking overall stats 26324 26325 this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment); // Note that the state isn't changed from loading to appending. This is because abort 26326 // logic may change behavior depending on the state, and changing state too early may 26327 // inflate our estimates of bandwidth. In the future this should be re-examined to 26328 // note more granular states. 26329 // don't process and append data if the mediaSource is closed 26330 26331 if (this.mediaSource_.readyState === 'closed') { 26332 return; 26333 } // if this request included an initialization segment, save that data 26334 // to the initSegment cache 26335 26336 26337 if (simpleSegment.map) { 26338 simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true); // move over init segment properties to media request 26339 26340 segmentInfo.segment.map = simpleSegment.map; 26341 } // if this request included a segment key, save that data in the cache 26342 26343 26344 if (simpleSegment.key) { 26345 this.segmentKey(simpleSegment.key, true); 26346 } 26347 26348 segmentInfo.isFmp4 = simpleSegment.isFmp4; 26349 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 26350 26351 if (segmentInfo.isFmp4) { 26352 this.trigger('fmp4'); 26353 segmentInfo.timingInfo.start = segmentInfo[timingInfoPropertyForMedia(result.type)].start; 26354 } else { 26355 const trackInfo = this.getCurrentMediaInfo_(); 26356 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 26357 let firstVideoFrameTimeForData; 26358 26359 if (useVideoTimingInfo) { 26360 firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start; 26361 } // Segment loader knows more about segment timing than the transmuxer (in certain 26362 // aspects), so make any changes required for a more accurate start time. 26363 // Don't set the end time yet, as the segment may not be finished processing. 26364 26365 26366 segmentInfo.timingInfo.start = this.trueSegmentStart_({ 26367 currentStart: segmentInfo.timingInfo.start, 26368 playlist: segmentInfo.playlist, 26369 mediaIndex: segmentInfo.mediaIndex, 26370 currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(), 26371 useVideoTimingInfo, 26372 firstVideoFrameTimeForData, 26373 videoTimingInfo: segmentInfo.videoTimingInfo, 26374 audioTimingInfo: segmentInfo.audioTimingInfo 26375 }); 26376 } // Init segments for audio and video only need to be appended in certain cases. Now 26377 // that data is about to be appended, we can check the final cases to determine 26378 // whether we should append an init segment. 26379 26380 26381 this.updateAppendInitSegmentStatus(segmentInfo, result.type); // Timestamp offset should be updated once we get new data and have its timing info, 26382 // as we use the start of the segment to offset the best guess (playlist provided) 26383 // timestamp offset. 26384 26385 this.updateSourceBufferTimestampOffset_(segmentInfo); // if this is a sync request we need to determine whether it should 26386 // be appended or not. 26387 26388 if (segmentInfo.isSyncRequest) { 26389 // first save/update our timing info for this segment. 26390 // this is what allows us to choose an accurate segment 26391 // and the main reason we make a sync request. 26392 this.updateTimingInfoEnd_(segmentInfo); 26393 this.syncController_.saveSegmentTimingInfo({ 26394 segmentInfo, 26395 shouldSaveTimelineMapping: this.loaderType_ === 'main' 26396 }); 26397 const next = this.chooseNextRequest_(); // If the sync request isn't the segment that would be requested next 26398 // after taking into account its timing info, do not append it. 26399 26400 if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) { 26401 this.logger_('sync segment was incorrect, not appending'); 26402 return; 26403 } // otherwise append it like any other segment as our guess was correct. 26404 26405 26406 this.logger_('sync segment was correct, appending'); 26407 } // Save some state so that in the future anything waiting on first append (and/or 26408 // timestamp offset(s)) can process immediately. While the extra state isn't optimal, 26409 // we need some notion of whether the timestamp offset or other relevant information 26410 // has had a chance to be set. 26411 26412 26413 segmentInfo.hasAppendedData_ = true; // Now that the timestamp offset should be set, we can append any waiting ID3 tags. 26414 26415 this.processMetadataQueue_(); 26416 this.appendData_(segmentInfo, result); 26417 } 26418 26419 updateAppendInitSegmentStatus(segmentInfo, type) { 26420 // alt audio doesn't manage timestamp offset 26421 if (this.loaderType_ === 'main' && typeof segmentInfo.timestampOffset === 'number' && // in the case that we're handling partial data, we don't want to append an init 26422 // segment for each chunk 26423 !segmentInfo.changedTimestampOffset) { 26424 // if the timestamp offset changed, the timeline may have changed, so we have to re- 26425 // append init segments 26426 this.appendInitSegment_ = { 26427 audio: true, 26428 video: true 26429 }; 26430 } 26431 26432 if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) { 26433 // make sure we append init segment on playlist changes, in case the media config 26434 // changed 26435 this.appendInitSegment_[type] = true; 26436 } 26437 } 26438 26439 getInitSegmentAndUpdateState_({ 26440 type, 26441 initSegment, 26442 map, 26443 playlist 26444 }) { 26445 // "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section 26446 // (Section 3) required to parse the applicable Media Segments. It applies to every 26447 // Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag 26448 // or until the end of the playlist." 26449 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5 26450 if (map) { 26451 const id = initSegmentId(map); 26452 26453 if (this.activeInitSegmentId_ === id) { 26454 // don't need to re-append the init segment if the ID matches 26455 return null; 26456 } // a map-specified init segment takes priority over any transmuxed (or otherwise 26457 // obtained) init segment 26458 // 26459 // this also caches the init segment for later use 26460 26461 26462 initSegment = this.initSegmentForMap(map, true).bytes; 26463 this.activeInitSegmentId_ = id; 26464 } // We used to always prepend init segments for video, however, that shouldn't be 26465 // necessary. Instead, we should only append on changes, similar to what we've always 26466 // done for audio. This is more important (though may not be that important) for 26467 // frame-by-frame appending for LHLS, simply because of the increased quantity of 26468 // appends. 26469 26470 26471 if (initSegment && this.appendInitSegment_[type]) { 26472 // Make sure we track the playlist that we last used for the init segment, so that 26473 // we can re-append the init segment in the event that we get data from a new 26474 // playlist. Discontinuities and track changes are handled in other sections. 26475 this.playlistOfLastInitSegment_[type] = playlist; // Disable future init segment appends for this type. Until a change is necessary. 26476 26477 this.appendInitSegment_[type] = false; // we need to clear out the fmp4 active init segment id, since 26478 // we are appending the muxer init segment 26479 26480 this.activeInitSegmentId_ = null; 26481 return initSegment; 26482 } 26483 26484 return null; 26485 } 26486 26487 handleQuotaExceededError_({ 26488 segmentInfo, 26489 type, 26490 bytes 26491 }, error) { 26492 const audioBuffered = this.sourceUpdater_.audioBuffered(); 26493 const videoBuffered = this.sourceUpdater_.videoBuffered(); // For now we're ignoring any notion of gaps in the buffer, but they, in theory, 26494 // should be cleared out during the buffer removals. However, log in case it helps 26495 // debug. 26496 26497 if (audioBuffered.length > 1) { 26498 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' + timeRangesToArray(audioBuffered).join(', ')); 26499 } 26500 26501 if (videoBuffered.length > 1) { 26502 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' + timeRangesToArray(videoBuffered).join(', ')); 26503 } 26504 26505 const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0; 26506 const audioBufferEnd = audioBuffered.length ? audioBuffered.end(audioBuffered.length - 1) : 0; 26507 const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0; 26508 const videoBufferEnd = videoBuffered.length ? videoBuffered.end(videoBuffered.length - 1) : 0; 26509 26510 if (audioBufferEnd - audioBufferStart <= MIN_BACK_BUFFER && videoBufferEnd - videoBufferStart <= MIN_BACK_BUFFER) { 26511 // Can't remove enough buffer to make room for new segment (or the browser doesn't 26512 // allow for appends of segments this size). In the future, it may be possible to 26513 // split up the segment and append in pieces, but for now, error out this playlist 26514 // in an attempt to switch to a more manageable rendition. 26515 this.logger_('On QUOTA_EXCEEDED_ERR, single segment too large to append to ' + 'buffer, triggering an error. ' + `Appended byte length: ${bytes.byteLength}, ` + `audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` + `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `); 26516 this.error({ 26517 message: 'Quota exceeded error with append of a single segment of content', 26518 excludeUntil: Infinity 26519 }); 26520 this.trigger('error'); 26521 return; 26522 } // To try to resolve the quota exceeded error, clear back buffer and retry. This means 26523 // that the segment-loader should block on future events until this one is handled, so 26524 // that it doesn't keep moving onto further segments. Adding the call to the call 26525 // queue will prevent further appends until waitingOnRemove_ and 26526 // quotaExceededErrorRetryTimeout_ are cleared. 26527 // 26528 // Note that this will only block the current loader. In the case of demuxed content, 26529 // the other load may keep filling as fast as possible. In practice, this should be 26530 // OK, as it is a rare case when either audio has a high enough bitrate to fill up a 26531 // source buffer, or video fills without enough room for audio to append (and without 26532 // the availability of clearing out seconds of back buffer to make room for audio). 26533 // But it might still be good to handle this case in the future as a TODO. 26534 26535 26536 this.waitingOnRemove_ = true; 26537 this.callQueue_.push(this.appendToSourceBuffer_.bind(this, { 26538 segmentInfo, 26539 type, 26540 bytes 26541 })); 26542 const currentTime = this.currentTime_(); // Try to remove as much audio and video as possible to make room for new content 26543 // before retrying. 26544 26545 const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER; 26546 this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`); 26547 this.remove(0, timeToRemoveUntil, () => { 26548 this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`); 26549 this.waitingOnRemove_ = false; // wait the length of time alotted in the back buffer to prevent wasted 26550 // attempts (since we can't clear less than the minimum) 26551 26552 this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => { 26553 this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue'); 26554 this.quotaExceededErrorRetryTimeout_ = null; 26555 this.processCallQueue_(); 26556 }, MIN_BACK_BUFFER * 1000); 26557 }, true); 26558 } 26559 26560 handleAppendError_({ 26561 segmentInfo, 26562 type, 26563 bytes 26564 }, error) { 26565 // if there's no error, nothing to do 26566 if (!error) { 26567 return; 26568 } 26569 26570 if (error.code === QUOTA_EXCEEDED_ERR) { 26571 this.handleQuotaExceededError_({ 26572 segmentInfo, 26573 type, 26574 bytes 26575 }); // A quota exceeded error should be recoverable with a future re-append, so no need 26576 // to trigger an append error. 26577 26578 return; 26579 } 26580 26581 this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error); // If an append errors, we often can't recover. 26582 // (see https://w3c.github.io/media-source/#sourcebuffer-append-error). 26583 // 26584 // Trigger a special error so that it can be handled separately from normal, 26585 // recoverable errors. 26586 26587 this.error({ 26588 message: `${type} append of ${bytes.length}b failed for segment ` + `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`, 26589 metadata: { 26590 errorType: videojs__default["default"].Error.StreamingFailedToAppendSegment 26591 } 26592 }); 26593 this.trigger('appenderror'); 26594 } 26595 26596 appendToSourceBuffer_({ 26597 segmentInfo, 26598 type, 26599 initSegment, 26600 data, 26601 bytes 26602 }) { 26603 // If this is a re-append, bytes were already created and don't need to be recreated 26604 if (!bytes) { 26605 const segments = [data]; 26606 let byteLength = data.byteLength; 26607 26608 if (initSegment) { 26609 // if the media initialization segment is changing, append it before the content 26610 // segment 26611 segments.unshift(initSegment); 26612 byteLength += initSegment.byteLength; 26613 } // Technically we should be OK appending the init segment separately, however, we 26614 // haven't yet tested that, and prepending is how we have always done things. 26615 26616 26617 bytes = concatSegments({ 26618 bytes: byteLength, 26619 segments 26620 }); 26621 } 26622 26623 const metadata = { 26624 segmentInfo: segmentInfoPayload({ 26625 type: this.loaderType_, 26626 segment: segmentInfo 26627 }) 26628 }; 26629 this.trigger({ 26630 type: 'segmentappendstart', 26631 metadata 26632 }); 26633 this.sourceUpdater_.appendBuffer({ 26634 segmentInfo, 26635 type, 26636 bytes 26637 }, this.handleAppendError_.bind(this, { 26638 segmentInfo, 26639 type, 26640 bytes 26641 })); 26642 } 26643 26644 handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) { 26645 if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) { 26646 return; 26647 } 26648 26649 const segment = this.pendingSegment_.segment; 26650 const timingInfoProperty = `${type}TimingInfo`; 26651 26652 if (!segment[timingInfoProperty]) { 26653 segment[timingInfoProperty] = {}; 26654 } 26655 26656 segment[timingInfoProperty].transmuxerPrependedSeconds = segmentTimingInfo.prependedContentDuration || 0; 26657 segment[timingInfoProperty].transmuxedPresentationStart = segmentTimingInfo.start.presentation; 26658 segment[timingInfoProperty].transmuxedDecodeStart = segmentTimingInfo.start.decode; 26659 segment[timingInfoProperty].transmuxedPresentationEnd = segmentTimingInfo.end.presentation; 26660 segment[timingInfoProperty].transmuxedDecodeEnd = segmentTimingInfo.end.decode; // mainly used as a reference for debugging 26661 26662 segment[timingInfoProperty].baseMediaDecodeTime = segmentTimingInfo.baseMediaDecodeTime; 26663 } 26664 26665 appendData_(segmentInfo, result) { 26666 const { 26667 type, 26668 data 26669 } = result; 26670 26671 if (!data || !data.byteLength) { 26672 return; 26673 } 26674 26675 if (type === 'audio' && this.audioDisabled_) { 26676 return; 26677 } 26678 26679 const initSegment = this.getInitSegmentAndUpdateState_({ 26680 type, 26681 initSegment: result.initSegment, 26682 playlist: segmentInfo.playlist, 26683 map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null 26684 }); 26685 this.appendToSourceBuffer_({ 26686 segmentInfo, 26687 type, 26688 initSegment, 26689 data 26690 }); 26691 } 26692 /** 26693 * load a specific segment from a request into the buffer 26694 * 26695 * @private 26696 */ 26697 26698 26699 loadSegment_(segmentInfo) { 26700 this.state = 'WAITING'; 26701 this.pendingSegment_ = segmentInfo; 26702 this.trimBackBuffer_(segmentInfo); 26703 26704 if (typeof segmentInfo.timestampOffset === 'number') { 26705 if (this.transmuxer_) { 26706 this.transmuxer_.postMessage({ 26707 action: 'clearAllMp4Captions' 26708 }); 26709 } 26710 } 26711 26712 if (!this.hasEnoughInfoToLoad_()) { 26713 checkAndFixTimelines(this); 26714 this.loadQueue_.push(() => { 26715 // regenerate the audioAppendStart, timestampOffset, etc as they 26716 // may have changed since this function was added to the queue. 26717 const options = _extends({}, segmentInfo, { 26718 forceTimestampOffset: true 26719 }); 26720 26721 _extends(segmentInfo, this.generateSegmentInfo_(options)); 26722 26723 this.isPendingTimestampOffset_ = false; 26724 this.updateTransmuxerAndRequestSegment_(segmentInfo); 26725 }); 26726 return; 26727 } 26728 26729 this.updateTransmuxerAndRequestSegment_(segmentInfo); 26730 } 26731 26732 updateTransmuxerAndRequestSegment_(segmentInfo) { 26733 // We'll update the source buffer's timestamp offset once we have transmuxed data, but 26734 // the transmuxer still needs to be updated before then. 26735 // 26736 // Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp 26737 // offset must be passed to the transmuxer for stream correcting adjustments. 26738 if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) { 26739 this.gopBuffer_.length = 0; // gopsToAlignWith was set before the GOP buffer was cleared 26740 26741 segmentInfo.gopsToAlignWith = []; 26742 this.timeMapping_ = 0; // reset values in the transmuxer since a discontinuity should start fresh 26743 26744 this.transmuxer_.postMessage({ 26745 action: 'reset' 26746 }); 26747 this.transmuxer_.postMessage({ 26748 action: 'setTimestampOffset', 26749 timestampOffset: segmentInfo.timestampOffset 26750 }); 26751 } 26752 26753 const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo); 26754 const isEndOfStream = this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex); 26755 const isWalkingForward = this.mediaIndex !== null; 26756 const isDiscontinuity = segmentInfo.timeline !== this.currentTimeline_ && // currentTimeline starts at -1, so we shouldn't end the timeline switching to 0, 26757 // the first timeline 26758 segmentInfo.timeline > 0; 26759 const isEndOfTimeline = isEndOfStream || isWalkingForward && isDiscontinuity; 26760 this.logger_(`Requesting 26761${compactSegmentUrlDescription(segmentInfo.uri)} 26762${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes), 26763 // then this init segment has never been seen before and should be appended. 26764 // 26765 // At this point the content type (audio/video or both) is not yet known, but it should be safe to set 26766 // both to true and leave the decision of whether to append the init segment to append time. 26767 26768 if (simpleSegment.map && !simpleSegment.map.bytes) { 26769 this.logger_('going to request init segment.'); 26770 this.appendInitSegment_ = { 26771 video: true, 26772 audio: true 26773 }; 26774 } 26775 26776 segmentInfo.abortRequests = mediaSegmentRequest({ 26777 xhr: this.vhs_.xhr, 26778 xhrOptions: this.xhrOptions_, 26779 decryptionWorker: this.decrypter_, 26780 segment: simpleSegment, 26781 abortFn: this.handleAbort_.bind(this, segmentInfo), 26782 progressFn: this.handleProgress_.bind(this), 26783 trackInfoFn: this.handleTrackInfo_.bind(this), 26784 timingInfoFn: this.handleTimingInfo_.bind(this), 26785 videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'video', segmentInfo.requestId), 26786 audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'audio', segmentInfo.requestId), 26787 captionsFn: this.handleCaptions_.bind(this), 26788 isEndOfTimeline, 26789 endedTimelineFn: () => { 26790 this.logger_('received endedtimeline callback'); 26791 }, 26792 id3Fn: this.handleId3_.bind(this), 26793 dataFn: this.handleData_.bind(this), 26794 doneFn: this.segmentRequestFinished_.bind(this), 26795 onTransmuxerLog: ({ 26796 message, 26797 level, 26798 stream 26799 }) => { 26800 this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); 26801 }, 26802 triggerSegmentEventFn: ({ 26803 type, 26804 segment, 26805 keyInfo, 26806 trackInfo, 26807 timingInfo 26808 }) => { 26809 const segInfo = segmentInfoPayload({ 26810 segment 26811 }); 26812 const metadata = { 26813 segmentInfo: segInfo 26814 }; // add other properties if necessary. 26815 26816 if (keyInfo) { 26817 metadata.keyInfo = keyInfo; 26818 } 26819 26820 if (trackInfo) { 26821 metadata.trackInfo = trackInfo; 26822 } 26823 26824 if (timingInfo) { 26825 metadata.timingInfo = timingInfo; 26826 } 26827 26828 this.trigger({ 26829 type, 26830 metadata 26831 }); 26832 } 26833 }); 26834 } 26835 /** 26836 * trim the back buffer so that we don't have too much data 26837 * in the source buffer 26838 * 26839 * @private 26840 * 26841 * @param {Object} segmentInfo - the current segment 26842 */ 26843 26844 26845 trimBackBuffer_(segmentInfo) { 26846 const removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); // Chrome has a hard limit of 150MB of 26847 // buffer and a very conservative "garbage collector" 26848 // We manually clear out the old buffer to ensure 26849 // we don't trigger the QuotaExceeded error 26850 // on the source buffer during subsequent appends 26851 26852 if (removeToTime > 0) { 26853 this.remove(0, removeToTime); 26854 } 26855 } 26856 /** 26857 * created a simplified copy of the segment object with just the 26858 * information necessary to perform the XHR and decryption 26859 * 26860 * @private 26861 * 26862 * @param {Object} segmentInfo - the current segment 26863 * @return {Object} a simplified segment object copy 26864 */ 26865 26866 26867 createSimplifiedSegmentObj_(segmentInfo) { 26868 const segment = segmentInfo.segment; 26869 const part = segmentInfo.part; 26870 const isEncrypted = segmentInfo.segment.key || segmentInfo.segment.map && segmentInfo.segment.map.key; 26871 const isMediaInitialization = segmentInfo.segment.map && !segmentInfo.segment.map.bytes; 26872 const simpleSegment = { 26873 resolvedUri: part ? part.resolvedUri : segment.resolvedUri, 26874 byterange: part ? part.byterange : segment.byterange, 26875 requestId: segmentInfo.requestId, 26876 transmuxer: segmentInfo.transmuxer, 26877 audioAppendStart: segmentInfo.audioAppendStart, 26878 gopsToAlignWith: segmentInfo.gopsToAlignWith, 26879 part: segmentInfo.part, 26880 type: this.loaderType_, 26881 start: segmentInfo.startOfSegment, 26882 duration: segmentInfo.duration, 26883 isEncrypted, 26884 isMediaInitialization 26885 }; 26886 const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1]; 26887 26888 if (previousSegment && previousSegment.timeline === segment.timeline) { 26889 // The baseStartTime of a segment is used to handle rollover when probing the TS 26890 // segment to retrieve timing information. Since the probe only looks at the media's 26891 // times (e.g., PTS and DTS values of the segment), and doesn't consider the 26892 // player's time (e.g., player.currentTime()), baseStartTime should reflect the 26893 // media time as well. transmuxedDecodeEnd represents the end time of a segment, in 26894 // seconds of media time, so should be used here. The previous segment is used since 26895 // the end of the previous segment should represent the beginning of the current 26896 // segment, so long as they are on the same timeline. 26897 if (previousSegment.videoTimingInfo) { 26898 simpleSegment.baseStartTime = previousSegment.videoTimingInfo.transmuxedDecodeEnd; 26899 } else if (previousSegment.audioTimingInfo) { 26900 simpleSegment.baseStartTime = previousSegment.audioTimingInfo.transmuxedDecodeEnd; 26901 } 26902 } 26903 26904 if (segment.key) { 26905 // if the media sequence is greater than 2^32, the IV will be incorrect 26906 // assuming 10s segments, that would be about 1300 years 26907 const iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 26908 simpleSegment.key = this.segmentKey(segment.key); 26909 simpleSegment.key.iv = iv; 26910 } 26911 26912 if (segment.map) { 26913 simpleSegment.map = this.initSegmentForMap(segment.map); 26914 } 26915 26916 return simpleSegment; 26917 } 26918 26919 saveTransferStats_(stats) { 26920 // every request counts as a media request even if it has been aborted 26921 // or canceled due to a timeout 26922 this.mediaRequests += 1; 26923 26924 if (stats) { 26925 this.mediaBytesTransferred += stats.bytesReceived; 26926 this.mediaTransferDuration += stats.roundTripTime; 26927 } 26928 } 26929 26930 saveBandwidthRelatedStats_(duration, stats) { 26931 // byteLength will be used for throughput, and should be based on bytes receieved, 26932 // which we only know at the end of the request and should reflect total bytes 26933 // downloaded rather than just bytes processed from components of the segment 26934 this.pendingSegment_.byteLength = stats.bytesReceived; 26935 26936 if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 26937 this.logger_(`Ignoring segment's bandwidth because its duration of ${duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`); 26938 return; 26939 } 26940 26941 const metadata = { 26942 bandwidthInfo: { 26943 from: this.bandwidth, 26944 to: stats.bandwidth 26945 } 26946 }; // player event with payload 26947 26948 this.trigger({ 26949 type: 'bandwidthupdated', 26950 metadata 26951 }); 26952 this.bandwidth = stats.bandwidth; 26953 this.roundTrip = stats.roundTripTime; 26954 } 26955 26956 handleTimeout_() { 26957 // although the VTT segment loader bandwidth isn't really used, it's good to 26958 // maintain functinality between segment loaders 26959 this.mediaRequestsTimedout += 1; 26960 this.bandwidth = 1; 26961 this.roundTrip = NaN; 26962 this.trigger('bandwidthupdate'); 26963 this.trigger('timeout'); 26964 } 26965 /** 26966 * Handle the callback from the segmentRequest function and set the 26967 * associated SegmentLoader state and errors if necessary 26968 * 26969 * @private 26970 */ 26971 26972 26973 segmentRequestFinished_(error, simpleSegment, result) { 26974 // TODO handle special cases, e.g., muxed audio/video but only audio in the segment 26975 // check the call queue directly since this function doesn't need to deal with any 26976 // data, and can continue even if the source buffers are not set up and we didn't get 26977 // any data from the segment 26978 if (this.callQueue_.length) { 26979 this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result)); 26980 return; 26981 } 26982 26983 this.saveTransferStats_(simpleSegment.stats); // The request was aborted and the SegmentLoader has already been reset 26984 26985 if (!this.pendingSegment_) { 26986 return; 26987 } // the request was aborted and the SegmentLoader has already started 26988 // another request. this can happen when the timeout for an aborted 26989 // request triggers due to a limitation in the XHR library 26990 // do not count this as any sort of request or we risk double-counting 26991 26992 26993 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 26994 return; 26995 } // an error occurred from the active pendingSegment_ so reset everything 26996 26997 26998 if (error) { 26999 this.pendingSegment_ = null; 27000 this.state = 'READY'; // aborts are not a true error condition and nothing corrective needs to be done 27001 27002 if (error.code === REQUEST_ERRORS.ABORTED) { 27003 return; 27004 } 27005 27006 this.pause(); // the error is really just that at least one of the requests timed-out 27007 // set the bandwidth to a very low value and trigger an ABR switch to 27008 // take emergency action 27009 27010 if (error.code === REQUEST_ERRORS.TIMEOUT) { 27011 this.handleTimeout_(); 27012 return; 27013 } // if control-flow has arrived here, then the error is real 27014 // emit an error event to exclude the current playlist 27015 27016 27017 this.mediaRequestsErrored += 1; 27018 this.error(error); 27019 this.trigger('error'); 27020 return; 27021 } 27022 27023 const segmentInfo = this.pendingSegment_; // the response was a success so set any bandwidth stats the request 27024 // generated for ABR purposes 27025 27026 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); 27027 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; 27028 27029 if (result.gopInfo) { 27030 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_); 27031 } // Although we may have already started appending on progress, we shouldn't switch the 27032 // state away from loading until we are officially done loading the segment data. 27033 27034 27035 this.state = 'APPENDING'; // used for testing 27036 27037 this.trigger('appending'); 27038 this.waitForAppendsToComplete_(segmentInfo); 27039 } 27040 27041 setTimeMapping_(timeline) { 27042 const timelineMapping = this.syncController_.mappingForTimeline(timeline); 27043 27044 if (timelineMapping !== null) { 27045 this.timeMapping_ = timelineMapping; 27046 } 27047 } 27048 27049 updateMediaSecondsLoaded_(segment) { 27050 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 27051 this.mediaSecondsLoaded += segment.end - segment.start; 27052 } else { 27053 this.mediaSecondsLoaded += segment.duration; 27054 } 27055 } 27056 27057 shouldUpdateTransmuxerTimestampOffset_(timestampOffset) { 27058 if (timestampOffset === null) { 27059 return false; 27060 } // note that we're potentially using the same timestamp offset for both video and 27061 // audio 27062 27063 27064 if (this.loaderType_ === 'main' && timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 27065 return true; 27066 } 27067 27068 if (!this.audioDisabled_ && timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 27069 return true; 27070 } 27071 27072 return false; 27073 } 27074 27075 trueSegmentStart_({ 27076 currentStart, 27077 playlist, 27078 mediaIndex, 27079 firstVideoFrameTimeForData, 27080 currentVideoTimestampOffset, 27081 useVideoTimingInfo, 27082 videoTimingInfo, 27083 audioTimingInfo 27084 }) { 27085 if (typeof currentStart !== 'undefined') { 27086 // if start was set once, keep using it 27087 return currentStart; 27088 } 27089 27090 if (!useVideoTimingInfo) { 27091 return audioTimingInfo.start; 27092 } 27093 27094 const previousSegment = playlist.segments[mediaIndex - 1]; // The start of a segment should be the start of the first full frame contained 27095 // within that segment. Since the transmuxer maintains a cache of incomplete data 27096 // from and/or the last frame seen, the start time may reflect a frame that starts 27097 // in the previous segment. Check for that case and ensure the start time is 27098 // accurate for the segment. 27099 27100 if (mediaIndex === 0 || !previousSegment || typeof previousSegment.start === 'undefined' || previousSegment.end !== firstVideoFrameTimeForData + currentVideoTimestampOffset) { 27101 return firstVideoFrameTimeForData; 27102 } 27103 27104 return videoTimingInfo.start; 27105 } 27106 27107 waitForAppendsToComplete_(segmentInfo) { 27108 const trackInfo = this.getCurrentMediaInfo_(segmentInfo); 27109 27110 if (!trackInfo) { 27111 this.error({ 27112 message: 'No starting media returned, likely due to an unsupported media format.', 27113 playlistExclusionDuration: Infinity 27114 }); 27115 this.trigger('error'); 27116 return; 27117 } // Although transmuxing is done, appends may not yet be finished. Throw a marker 27118 // on each queue this loader is responsible for to ensure that the appends are 27119 // complete. 27120 27121 27122 const { 27123 hasAudio, 27124 hasVideo, 27125 isMuxed 27126 } = trackInfo; 27127 const waitForVideo = this.loaderType_ === 'main' && hasVideo; 27128 const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed; 27129 segmentInfo.waitingOnAppends = 0; // segments with no data 27130 27131 if (!segmentInfo.hasAppendedData_) { 27132 if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') { 27133 // When there's no audio or video data in the segment, there's no audio or video 27134 // timing information. 27135 // 27136 // If there's no audio or video timing information, then the timestamp offset 27137 // can't be adjusted to the appropriate value for the transmuxer and source 27138 // buffers. 27139 // 27140 // Therefore, the next segment should be used to set the timestamp offset. 27141 this.isPendingTimestampOffset_ = true; 27142 } // override settings for metadata only segments 27143 27144 27145 segmentInfo.timingInfo = { 27146 start: 0 27147 }; 27148 segmentInfo.waitingOnAppends++; 27149 27150 if (!this.isPendingTimestampOffset_) { 27151 // update the timestampoffset 27152 this.updateSourceBufferTimestampOffset_(segmentInfo); // make sure the metadata queue is processed even though we have 27153 // no video/audio data. 27154 27155 this.processMetadataQueue_(); 27156 } // append is "done" instantly with no data. 27157 27158 27159 this.checkAppendsDone_(segmentInfo); 27160 return; 27161 } // Since source updater could call back synchronously, do the increments first. 27162 27163 27164 if (waitForVideo) { 27165 segmentInfo.waitingOnAppends++; 27166 } 27167 27168 if (waitForAudio) { 27169 segmentInfo.waitingOnAppends++; 27170 } 27171 27172 if (waitForVideo) { 27173 this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 27174 } 27175 27176 if (waitForAudio) { 27177 this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 27178 } 27179 } 27180 27181 checkAppendsDone_(segmentInfo) { 27182 if (this.checkForAbort_(segmentInfo.requestId)) { 27183 return; 27184 } 27185 27186 segmentInfo.waitingOnAppends--; 27187 27188 if (segmentInfo.waitingOnAppends === 0) { 27189 this.handleAppendsDone_(); 27190 } 27191 } 27192 27193 checkForIllegalMediaSwitch(trackInfo) { 27194 const illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.getCurrentMediaInfo_(), trackInfo); 27195 27196 if (illegalMediaSwitchError) { 27197 this.error({ 27198 message: illegalMediaSwitchError, 27199 playlistExclusionDuration: Infinity 27200 }); 27201 this.trigger('error'); 27202 return true; 27203 } 27204 27205 return false; 27206 } 27207 27208 updateSourceBufferTimestampOffset_(segmentInfo) { 27209 if (segmentInfo.timestampOffset === null || // we don't yet have the start for whatever media type (video or audio) has 27210 // priority, timing-wise, so we must wait 27211 typeof segmentInfo.timingInfo.start !== 'number' || // already updated the timestamp offset for this segment 27212 segmentInfo.changedTimestampOffset || // the alt audio loader should not be responsible for setting the timestamp offset 27213 this.loaderType_ !== 'main') { 27214 return; 27215 } 27216 27217 let didChange = false; // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that 27218 // the timing info here comes from video. In the event that the audio is longer than 27219 // the video, this will trim the start of the audio. 27220 // This also trims any offset from 0 at the beginning of the media 27221 27222 segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({ 27223 videoTimingInfo: segmentInfo.segment.videoTimingInfo, 27224 audioTimingInfo: segmentInfo.segment.audioTimingInfo, 27225 timingInfo: segmentInfo.timingInfo 27226 }); // In the event that there are part segment downloads, each will try to update the 27227 // timestamp offset. Retaining this bit of state prevents us from updating in the 27228 // future (within the same segment), however, there may be a better way to handle it. 27229 27230 segmentInfo.changedTimestampOffset = true; 27231 27232 if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 27233 this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset); 27234 didChange = true; 27235 } 27236 27237 if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 27238 this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset); 27239 didChange = true; 27240 } 27241 27242 if (didChange) { 27243 this.trigger('timestampoffset'); 27244 } 27245 } 27246 27247 getSegmentStartTimeForTimestampOffsetCalculation_({ 27248 videoTimingInfo, 27249 audioTimingInfo, 27250 timingInfo 27251 }) { 27252 if (!this.useDtsForTimestampOffset_) { 27253 return timingInfo.start; 27254 } 27255 27256 if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') { 27257 return videoTimingInfo.transmuxedDecodeStart; 27258 } // handle audio only 27259 27260 27261 if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') { 27262 return audioTimingInfo.transmuxedDecodeStart; 27263 } // handle content not transmuxed (e.g., MP4) 27264 27265 27266 return timingInfo.start; 27267 } 27268 27269 updateTimingInfoEnd_(segmentInfo) { 27270 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 27271 const trackInfo = this.getMediaInfo_(); 27272 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 27273 const prioritizedTimingInfo = useVideoTimingInfo && segmentInfo.videoTimingInfo ? segmentInfo.videoTimingInfo : segmentInfo.audioTimingInfo; 27274 27275 if (!prioritizedTimingInfo) { 27276 return; 27277 } 27278 27279 segmentInfo.timingInfo.end = typeof prioritizedTimingInfo.end === 'number' ? // End time may not exist in a case where we aren't parsing the full segment (one 27280 // current example is the case of fmp4), so use the rough duration to calculate an 27281 // end time. 27282 prioritizedTimingInfo.end : prioritizedTimingInfo.start + segmentInfo.duration; 27283 } 27284 /** 27285 * callback to run when appendBuffer is finished. detects if we are 27286 * in a good state to do things with the data we got, or if we need 27287 * to wait for more 27288 * 27289 * @private 27290 */ 27291 27292 27293 handleAppendsDone_() { 27294 // appendsdone can cause an abort 27295 if (this.pendingSegment_) { 27296 const metadata = { 27297 segmentInfo: segmentInfoPayload({ 27298 type: this.loaderType_, 27299 segment: this.pendingSegment_ 27300 }) 27301 }; 27302 this.trigger({ 27303 type: 'appendsdone', 27304 metadata 27305 }); 27306 } 27307 27308 if (!this.pendingSegment_) { 27309 this.state = 'READY'; // TODO should this move into this.checkForAbort to speed up requests post abort in 27310 // all appending cases? 27311 27312 if (!this.paused()) { 27313 this.monitorBuffer_(); 27314 } 27315 27316 return; 27317 } 27318 27319 const segmentInfo = this.pendingSegment_; 27320 27321 if (segmentInfo.part && segmentInfo.part.syncInfo) { 27322 // low-latency flow 27323 segmentInfo.part.syncInfo.markAppended(); 27324 } else if (segmentInfo.segment.syncInfo) { 27325 // normal flow 27326 segmentInfo.segment.syncInfo.markAppended(); 27327 } // Now that the end of the segment has been reached, we can set the end time. It's 27328 // best to wait until all appends are done so we're sure that the primary media is 27329 // finished (and we have its end time). 27330 27331 27332 this.updateTimingInfoEnd_(segmentInfo); 27333 27334 if (this.shouldSaveSegmentTimingInfo_) { 27335 // Timeline mappings should only be saved for the main loader. This is for multiple 27336 // reasons: 27337 // 27338 // 1) Only one mapping is saved per timeline, meaning that if both the audio loader 27339 // and the main loader try to save the timeline mapping, whichever comes later 27340 // will overwrite the first. In theory this is OK, as the mappings should be the 27341 // same, however, it breaks for (2) 27342 // 2) In the event of a live stream, the initial live point will make for a somewhat 27343 // arbitrary mapping. If audio and video streams are not perfectly in-sync, then 27344 // the mapping will be off for one of the streams, dependent on which one was 27345 // first saved (see (1)). 27346 // 3) Primary timing goes by video in VHS, so the mapping should be video. 27347 // 27348 // Since the audio loader will wait for the main loader to load the first segment, 27349 // the main loader will save the first timeline mapping, and ensure that there won't 27350 // be a case where audio loads two segments without saving a mapping (thus leading 27351 // to missing segment timing info). 27352 this.syncController_.saveSegmentTimingInfo({ 27353 segmentInfo, 27354 shouldSaveTimelineMapping: this.loaderType_ === 'main' 27355 }); 27356 } 27357 27358 const segmentDurationMessage = getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_); 27359 27360 if (segmentDurationMessage) { 27361 if (segmentDurationMessage.severity === 'warn') { 27362 videojs__default["default"].log.warn(segmentDurationMessage.message); 27363 } else { 27364 this.logger_(segmentDurationMessage.message); 27365 } 27366 } 27367 27368 this.recordThroughput_(segmentInfo); 27369 this.pendingSegment_ = null; 27370 this.state = 'READY'; 27371 27372 if (segmentInfo.isSyncRequest) { 27373 this.trigger('syncinfoupdate'); // if the sync request was not appended 27374 // then it was not the correct segment. 27375 // throw it away and use the data it gave us 27376 // to get the correct one. 27377 27378 if (!segmentInfo.hasAppendedData_) { 27379 this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`); 27380 return; 27381 } 27382 } 27383 27384 this.logger_(`Appended ${segmentInfoString(segmentInfo)}`); 27385 this.addSegmentMetadataCue_(segmentInfo); 27386 this.fetchAtBuffer_ = true; 27387 27388 if (this.currentTimeline_ !== segmentInfo.timeline) { 27389 this.timelineChangeController_.lastTimelineChange({ 27390 type: this.loaderType_, 27391 from: this.currentTimeline_, 27392 to: segmentInfo.timeline 27393 }); // If audio is not disabled, the main segment loader is responsible for updating 27394 // the audio timeline as well. If the content is video only, this won't have any 27395 // impact. 27396 27397 if (this.loaderType_ === 'main' && !this.audioDisabled_) { 27398 this.timelineChangeController_.lastTimelineChange({ 27399 type: 'audio', 27400 from: this.currentTimeline_, 27401 to: segmentInfo.timeline 27402 }); 27403 } 27404 } 27405 27406 this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before 27407 // the following conditional otherwise it may consider this a bad "guess" 27408 // and attempt to resync when the post-update seekable window and live 27409 // point would mean that this was the perfect segment to fetch 27410 27411 this.trigger('syncinfoupdate'); 27412 const segment = segmentInfo.segment; 27413 const part = segmentInfo.part; 27414 const badSegmentGuess = segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3; 27415 const badPartGuess = part && part.end && this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3; // If we previously appended a segment/part that ends more than 3 part/targetDurations before 27416 // the currentTime_ that means that our conservative guess was too conservative. 27417 // In that case, reset the loader state so that we try to use any information gained 27418 // from the previous request to create a new, more accurate, sync-point. 27419 27420 if (badSegmentGuess || badPartGuess) { 27421 this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`); 27422 this.resetEverything(); 27423 return; 27424 } 27425 27426 const isWalkingForward = this.mediaIndex !== null; // Don't do a rendition switch unless we have enough time to get a sync segment 27427 // and conservatively guess 27428 27429 if (isWalkingForward) { 27430 this.trigger('bandwidthupdate'); 27431 } 27432 27433 this.trigger('progress'); 27434 this.mediaIndex = segmentInfo.mediaIndex; 27435 this.partIndex = segmentInfo.partIndex; // any time an update finishes and the last segment is in the 27436 // buffer, end the stream. this ensures the "ended" event will 27437 // fire if playback reaches that point. 27438 27439 if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) { 27440 this.endOfStream(); 27441 } // used for testing 27442 27443 27444 this.trigger('appended'); 27445 27446 if (segmentInfo.hasAppendedData_) { 27447 this.mediaAppends++; 27448 } 27449 27450 if (!this.paused()) { 27451 this.monitorBuffer_(); 27452 } 27453 } 27454 /** 27455 * Records the current throughput of the decrypt, transmux, and append 27456 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 27457 * moving average of the throughput. `throughput.count` is the number of 27458 * data points in the average. 27459 * 27460 * @private 27461 * @param {Object} segmentInfo the object returned by loadSegment 27462 */ 27463 27464 27465 recordThroughput_(segmentInfo) { 27466 if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 27467 this.logger_(`Ignoring segment's throughput because its duration of ${segmentInfo.duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`); 27468 return; 27469 } 27470 27471 const rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide 27472 // by zero in the case where the throughput is ridiculously high 27473 27474 const segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second 27475 27476 const segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); // This is just a cumulative moving average calculation: 27477 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 27478 27479 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 27480 } 27481 /** 27482 * Adds a cue to the segment-metadata track with some metadata information about the 27483 * segment 27484 * 27485 * @private 27486 * @param {Object} segmentInfo 27487 * the object returned by loadSegment 27488 * @method addSegmentMetadataCue_ 27489 */ 27490 27491 27492 addSegmentMetadataCue_(segmentInfo) { 27493 if (!this.segmentMetadataTrack_) { 27494 return; 27495 } 27496 27497 const segment = segmentInfo.segment; 27498 const start = segment.start; 27499 const end = segment.end; // Do not try adding the cue if the start and end times are invalid. 27500 27501 if (!finite(start) || !finite(end)) { 27502 return; 27503 } 27504 27505 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 27506 const Cue = window.WebKitDataCue || window.VTTCue; 27507 const value = { 27508 custom: segment.custom, 27509 dateTimeObject: segment.dateTimeObject, 27510 dateTimeString: segment.dateTimeString, 27511 programDateTime: segment.programDateTime, 27512 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 27513 resolution: segmentInfo.playlist.attributes.RESOLUTION, 27514 codecs: segmentInfo.playlist.attributes.CODECS, 27515 byteLength: segmentInfo.byteLength, 27516 uri: segmentInfo.uri, 27517 timeline: segmentInfo.timeline, 27518 playlist: segmentInfo.playlist.id, 27519 start, 27520 end 27521 }; 27522 const data = JSON.stringify(value); 27523 const cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between 27524 // the differences of WebKitDataCue in safari and VTTCue in other browsers 27525 27526 cue.value = value; 27527 this.segmentMetadataTrack_.addCue(cue); 27528 } 27529 27530 } 27531 27532 function noop() {} 27533 27534 const toTitleCase = function (string) { 27535 if (typeof string !== 'string') { 27536 return string; 27537 } 27538 27539 return string.replace(/./, w => w.toUpperCase()); 27540 }; 27541 27542 /** 27543 * @file source-updater.js 27544 */ 27545 const bufferTypes = ['video', 'audio']; 27546 27547 const updating = (type, sourceUpdater) => { 27548 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 27549 return sourceBuffer && sourceBuffer.updating || sourceUpdater.queuePending[type]; 27550 }; 27551 27552 const nextQueueIndexOfType = (type, queue) => { 27553 for (let i = 0; i < queue.length; i++) { 27554 const queueEntry = queue[i]; 27555 27556 if (queueEntry.type === 'mediaSource') { 27557 // If the next entry is a media source entry (uses multiple source buffers), block 27558 // processing to allow it to go through first. 27559 return null; 27560 } 27561 27562 if (queueEntry.type === type) { 27563 return i; 27564 } 27565 } 27566 27567 return null; 27568 }; 27569 27570 const shiftQueue = (type, sourceUpdater) => { 27571 if (sourceUpdater.queue.length === 0) { 27572 return; 27573 } 27574 27575 let queueIndex = 0; 27576 let queueEntry = sourceUpdater.queue[queueIndex]; 27577 27578 if (queueEntry.type === 'mediaSource') { 27579 if (!sourceUpdater.updating() && sourceUpdater.mediaSource.readyState !== 'closed') { 27580 sourceUpdater.queue.shift(); 27581 queueEntry.action(sourceUpdater); 27582 27583 if (queueEntry.doneFn) { 27584 queueEntry.doneFn(); 27585 } // Only specific source buffer actions must wait for async updateend events. Media 27586 // Source actions process synchronously. Therefore, both audio and video source 27587 // buffers are now clear to process the next queue entries. 27588 27589 27590 shiftQueue('audio', sourceUpdater); 27591 shiftQueue('video', sourceUpdater); 27592 } // Media Source actions require both source buffers, so if the media source action 27593 // couldn't process yet (because one or both source buffers are busy), block other 27594 // queue actions until both are available and the media source action can process. 27595 27596 27597 return; 27598 } 27599 27600 if (type === 'mediaSource') { 27601 // If the queue was shifted by a media source action (this happens when pushing a 27602 // media source action onto the queue), then it wasn't from an updateend event from an 27603 // audio or video source buffer, so there's no change from previous state, and no 27604 // processing should be done. 27605 return; 27606 } // Media source queue entries don't need to consider whether the source updater is 27607 // started (i.e., source buffers are created) as they don't need the source buffers, but 27608 // source buffer queue entries do. 27609 27610 27611 if (!sourceUpdater.ready() || sourceUpdater.mediaSource.readyState === 'closed' || updating(type, sourceUpdater)) { 27612 return; 27613 } 27614 27615 if (queueEntry.type !== type) { 27616 queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue); 27617 27618 if (queueIndex === null) { 27619 // Either there's no queue entry that uses this source buffer type in the queue, or 27620 // there's a media source queue entry before the next entry of this type, in which 27621 // case wait for that action to process first. 27622 return; 27623 } 27624 27625 queueEntry = sourceUpdater.queue[queueIndex]; 27626 } 27627 27628 sourceUpdater.queue.splice(queueIndex, 1); // Keep a record that this source buffer type is in use. 27629 // 27630 // The queue pending operation must be set before the action is performed in the event 27631 // that the action results in a synchronous event that is acted upon. For instance, if 27632 // an exception is thrown that can be handled, it's possible that new actions will be 27633 // appended to an empty queue and immediately executed, but would not have the correct 27634 // pending information if this property was set after the action was performed. 27635 27636 sourceUpdater.queuePending[type] = queueEntry; 27637 queueEntry.action(type, sourceUpdater); 27638 27639 if (!queueEntry.doneFn) { 27640 // synchronous operation, process next entry 27641 sourceUpdater.queuePending[type] = null; 27642 shiftQueue(type, sourceUpdater); 27643 return; 27644 } 27645 }; 27646 27647 const cleanupBuffer = (type, sourceUpdater) => { 27648 const buffer = sourceUpdater[`${type}Buffer`]; 27649 const titleType = toTitleCase(type); 27650 27651 if (!buffer) { 27652 return; 27653 } 27654 27655 buffer.removeEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 27656 buffer.removeEventListener('error', sourceUpdater[`on${titleType}Error_`]); 27657 sourceUpdater.codecs[type] = null; 27658 sourceUpdater[`${type}Buffer`] = null; 27659 }; 27660 27661 const inSourceBuffers = (mediaSource, sourceBuffer) => mediaSource && sourceBuffer && Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1; 27662 27663 const actions = { 27664 appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => { 27665 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 27666 // or the media source does not contain this source buffer. 27667 27668 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 27669 return; 27670 } 27671 27672 sourceUpdater.logger_(`Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer`); 27673 27674 try { 27675 sourceBuffer.appendBuffer(bytes); 27676 } catch (e) { 27677 sourceUpdater.logger_(`Error with code ${e.code} ` + (e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') + `when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer`); 27678 sourceUpdater.queuePending[type] = null; 27679 onError(e); 27680 } 27681 }, 27682 remove: (start, end) => (type, sourceUpdater) => { 27683 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 27684 // or the media source does not contain this source buffer. 27685 27686 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 27687 return; 27688 } 27689 27690 sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`); 27691 27692 try { 27693 sourceBuffer.remove(start, end); 27694 } catch (e) { 27695 sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`); 27696 } 27697 }, 27698 timestampOffset: offset => (type, sourceUpdater) => { 27699 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 27700 // or the media source does not contain this source buffer. 27701 27702 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 27703 return; 27704 } 27705 27706 sourceUpdater.logger_(`Setting ${type}timestampOffset to ${offset}`); 27707 sourceBuffer.timestampOffset = offset; 27708 }, 27709 callback: callback => (type, sourceUpdater) => { 27710 callback(); 27711 }, 27712 endOfStream: error => sourceUpdater => { 27713 if (sourceUpdater.mediaSource.readyState !== 'open') { 27714 return; 27715 } 27716 27717 sourceUpdater.logger_(`Calling mediaSource endOfStream(${error || ''})`); 27718 27719 try { 27720 sourceUpdater.mediaSource.endOfStream(error); 27721 } catch (e) { 27722 videojs__default["default"].log.warn('Failed to call media source endOfStream', e); 27723 } 27724 }, 27725 duration: duration => sourceUpdater => { 27726 sourceUpdater.logger_(`Setting mediaSource duration to ${duration}`); 27727 27728 try { 27729 sourceUpdater.mediaSource.duration = duration; 27730 } catch (e) { 27731 videojs__default["default"].log.warn('Failed to set media source duration', e); 27732 } 27733 }, 27734 abort: () => (type, sourceUpdater) => { 27735 if (sourceUpdater.mediaSource.readyState !== 'open') { 27736 return; 27737 } 27738 27739 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 27740 // or the media source does not contain this source buffer. 27741 27742 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 27743 return; 27744 } 27745 27746 sourceUpdater.logger_(`calling abort on ${type}Buffer`); 27747 27748 try { 27749 sourceBuffer.abort(); 27750 } catch (e) { 27751 videojs__default["default"].log.warn(`Failed to abort on ${type}Buffer`, e); 27752 } 27753 }, 27754 addSourceBuffer: (type, codec) => sourceUpdater => { 27755 const titleType = toTitleCase(type); 27756 const mime = getMimeForCodec(codec); 27757 sourceUpdater.logger_(`Adding ${type}Buffer with codec ${codec} to mediaSource`); 27758 const sourceBuffer = sourceUpdater.mediaSource.addSourceBuffer(mime); 27759 sourceBuffer.addEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 27760 sourceBuffer.addEventListener('error', sourceUpdater[`on${titleType}Error_`]); 27761 sourceUpdater.codecs[type] = codec; 27762 sourceUpdater[`${type}Buffer`] = sourceBuffer; 27763 }, 27764 removeSourceBuffer: type => sourceUpdater => { 27765 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 27766 cleanupBuffer(type, sourceUpdater); // can't do anything if the media source / source buffer is null 27767 // or the media source does not contain this source buffer. 27768 27769 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 27770 return; 27771 } 27772 27773 sourceUpdater.logger_(`Removing ${type}Buffer with codec ${sourceUpdater.codecs[type]} from mediaSource`); 27774 27775 try { 27776 sourceUpdater.mediaSource.removeSourceBuffer(sourceBuffer); 27777 } catch (e) { 27778 videojs__default["default"].log.warn(`Failed to removeSourceBuffer ${type}Buffer`, e); 27779 } 27780 }, 27781 changeType: codec => (type, sourceUpdater) => { 27782 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 27783 const mime = getMimeForCodec(codec); // can't do anything if the media source / source buffer is null 27784 // or the media source does not contain this source buffer. 27785 27786 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 27787 return; 27788 } // do not update codec if we don't need to. 27789 // Only update if we change the codec base. 27790 // For example, going from avc1.640028 to avc1.64001f does not require a changeType call. 27791 27792 27793 const newCodecBase = codec.substring(0, codec.indexOf('.')); 27794 const oldCodec = sourceUpdater.codecs[type]; 27795 const oldCodecBase = oldCodec.substring(0, oldCodec.indexOf('.')); 27796 27797 if (oldCodecBase === newCodecBase) { 27798 return; 27799 } 27800 27801 const metadata = { 27802 codecsChangeInfo: { 27803 from: oldCodec, 27804 to: codec 27805 } 27806 }; 27807 sourceUpdater.trigger({ 27808 type: 'codecschange', 27809 metadata 27810 }); 27811 sourceUpdater.logger_(`changing ${type}Buffer codec from ${oldCodec} to ${codec}`); // check if change to the provided type is supported 27812 27813 try { 27814 sourceBuffer.changeType(mime); 27815 sourceUpdater.codecs[type] = codec; 27816 } catch (e) { 27817 metadata.errorType = videojs__default["default"].Error.StreamingCodecsChangeError; 27818 metadata.error = e; 27819 e.metadata = metadata; 27820 sourceUpdater.error_ = e; 27821 sourceUpdater.trigger('error'); 27822 videojs__default["default"].log.warn(`Failed to changeType on ${type}Buffer`, e); 27823 } 27824 } 27825 }; 27826 27827 const pushQueue = ({ 27828 type, 27829 sourceUpdater, 27830 action, 27831 doneFn, 27832 name 27833 }) => { 27834 sourceUpdater.queue.push({ 27835 type, 27836 action, 27837 doneFn, 27838 name 27839 }); 27840 shiftQueue(type, sourceUpdater); 27841 }; 27842 27843 const onUpdateend = (type, sourceUpdater) => e => { 27844 // Although there should, in theory, be a pending action for any updateend receieved, 27845 // there are some actions that may trigger updateend events without set definitions in 27846 // the w3c spec. For instance, setting the duration on the media source may trigger 27847 // updateend events on source buffers. This does not appear to be in the spec. As such, 27848 // if we encounter an updateend without a corresponding pending action from our queue 27849 // for that source buffer type, process the next action. 27850 const bufferedRangesForType = sourceUpdater[`${type}Buffered`](); 27851 const descriptiveString = bufferedRangesToString(bufferedRangesForType); 27852 sourceUpdater.logger_(`received "updateend" event for ${type} Source Buffer: `, descriptiveString); 27853 27854 if (sourceUpdater.queuePending[type]) { 27855 const doneFn = sourceUpdater.queuePending[type].doneFn; 27856 sourceUpdater.queuePending[type] = null; 27857 27858 if (doneFn) { 27859 // if there's an error, report it 27860 doneFn(sourceUpdater[`${type}Error_`]); 27861 } 27862 } 27863 27864 shiftQueue(type, sourceUpdater); 27865 }; 27866 /** 27867 * A queue of callbacks to be serialized and applied when a 27868 * MediaSource and its associated SourceBuffers are not in the 27869 * updating state. It is used by the segment loader to update the 27870 * underlying SourceBuffers when new data is loaded, for instance. 27871 * 27872 * @class SourceUpdater 27873 * @param {MediaSource} mediaSource the MediaSource to create the SourceBuffer from 27874 * @param {string} mimeType the desired MIME type of the underlying SourceBuffer 27875 */ 27876 27877 27878 class SourceUpdater extends videojs__default["default"].EventTarget { 27879 constructor(mediaSource) { 27880 super(); 27881 this.mediaSource = mediaSource; 27882 27883 this.sourceopenListener_ = () => shiftQueue('mediaSource', this); 27884 27885 this.mediaSource.addEventListener('sourceopen', this.sourceopenListener_); 27886 this.logger_ = logger('SourceUpdater'); // initial timestamp offset is 0 27887 27888 this.audioTimestampOffset_ = 0; 27889 this.videoTimestampOffset_ = 0; 27890 this.queue = []; 27891 this.queuePending = { 27892 audio: null, 27893 video: null 27894 }; 27895 this.delayedAudioAppendQueue_ = []; 27896 this.videoAppendQueued_ = false; 27897 this.codecs = {}; 27898 this.onVideoUpdateEnd_ = onUpdateend('video', this); 27899 this.onAudioUpdateEnd_ = onUpdateend('audio', this); 27900 27901 this.onVideoError_ = e => { 27902 // used for debugging 27903 this.videoError_ = e; 27904 }; 27905 27906 this.onAudioError_ = e => { 27907 // used for debugging 27908 this.audioError_ = e; 27909 }; 27910 27911 this.createdSourceBuffers_ = false; 27912 this.initializedEme_ = false; 27913 this.triggeredReady_ = false; 27914 } 27915 27916 initializedEme() { 27917 this.initializedEme_ = true; 27918 this.triggerReady(); 27919 } 27920 27921 hasCreatedSourceBuffers() { 27922 // if false, likely waiting on one of the segment loaders to get enough data to create 27923 // source buffers 27924 return this.createdSourceBuffers_; 27925 } 27926 27927 hasInitializedAnyEme() { 27928 return this.initializedEme_; 27929 } 27930 27931 ready() { 27932 return this.hasCreatedSourceBuffers() && this.hasInitializedAnyEme(); 27933 } 27934 27935 createSourceBuffers(codecs) { 27936 if (this.hasCreatedSourceBuffers()) { 27937 // already created them before 27938 return; 27939 } // the intial addOrChangeSourceBuffers will always be 27940 // two add buffers. 27941 27942 27943 this.addOrChangeSourceBuffers(codecs); 27944 this.createdSourceBuffers_ = true; 27945 this.trigger('createdsourcebuffers'); 27946 this.triggerReady(); 27947 } 27948 27949 triggerReady() { 27950 // only allow ready to be triggered once, this prevents the case 27951 // where: 27952 // 1. we trigger createdsourcebuffers 27953 // 2. ie 11 synchronously initializates eme 27954 // 3. the synchronous initialization causes us to trigger ready 27955 // 4. We go back to the ready check in createSourceBuffers and ready is triggered again. 27956 if (this.ready() && !this.triggeredReady_) { 27957 this.triggeredReady_ = true; 27958 this.trigger('ready'); 27959 } 27960 } 27961 /** 27962 * Add a type of source buffer to the media source. 27963 * 27964 * @param {string} type 27965 * The type of source buffer to add. 27966 * 27967 * @param {string} codec 27968 * The codec to add the source buffer with. 27969 */ 27970 27971 27972 addSourceBuffer(type, codec) { 27973 pushQueue({ 27974 type: 'mediaSource', 27975 sourceUpdater: this, 27976 action: actions.addSourceBuffer(type, codec), 27977 name: 'addSourceBuffer' 27978 }); 27979 } 27980 /** 27981 * call abort on a source buffer. 27982 * 27983 * @param {string} type 27984 * The type of source buffer to call abort on. 27985 */ 27986 27987 27988 abort(type) { 27989 pushQueue({ 27990 type, 27991 sourceUpdater: this, 27992 action: actions.abort(type), 27993 name: 'abort' 27994 }); 27995 } 27996 /** 27997 * Call removeSourceBuffer and remove a specific type 27998 * of source buffer on the mediaSource. 27999 * 28000 * @param {string} type 28001 * The type of source buffer to remove. 28002 */ 28003 28004 28005 removeSourceBuffer(type) { 28006 if (!this.canRemoveSourceBuffer()) { 28007 videojs__default["default"].log.error('removeSourceBuffer is not supported!'); 28008 return; 28009 } 28010 28011 pushQueue({ 28012 type: 'mediaSource', 28013 sourceUpdater: this, 28014 action: actions.removeSourceBuffer(type), 28015 name: 'removeSourceBuffer' 28016 }); 28017 } 28018 /** 28019 * Whether or not the removeSourceBuffer function is supported 28020 * on the mediaSource. 28021 * 28022 * @return {boolean} 28023 * if removeSourceBuffer can be called. 28024 */ 28025 28026 28027 canRemoveSourceBuffer() { 28028 // As of Firefox 83 removeSourceBuffer 28029 // throws errors, so we report that it does not support this. 28030 return !videojs__default["default"].browser.IS_FIREFOX && window.MediaSource && window.MediaSource.prototype && typeof window.MediaSource.prototype.removeSourceBuffer === 'function'; 28031 } 28032 /** 28033 * Whether or not the changeType function is supported 28034 * on our SourceBuffers. 28035 * 28036 * @return {boolean} 28037 * if changeType can be called. 28038 */ 28039 28040 28041 static canChangeType() { 28042 return window.SourceBuffer && window.SourceBuffer.prototype && typeof window.SourceBuffer.prototype.changeType === 'function'; 28043 } 28044 /** 28045 * Whether or not the changeType function is supported 28046 * on our SourceBuffers. 28047 * 28048 * @return {boolean} 28049 * if changeType can be called. 28050 */ 28051 28052 28053 canChangeType() { 28054 return this.constructor.canChangeType(); 28055 } 28056 /** 28057 * Call the changeType function on a source buffer, given the code and type. 28058 * 28059 * @param {string} type 28060 * The type of source buffer to call changeType on. 28061 * 28062 * @param {string} codec 28063 * The codec string to change type with on the source buffer. 28064 */ 28065 28066 28067 changeType(type, codec) { 28068 if (!this.canChangeType()) { 28069 videojs__default["default"].log.error('changeType is not supported!'); 28070 return; 28071 } 28072 28073 pushQueue({ 28074 type, 28075 sourceUpdater: this, 28076 action: actions.changeType(codec), 28077 name: 'changeType' 28078 }); 28079 } 28080 /** 28081 * Add source buffers with a codec or, if they are already created, 28082 * call changeType on source buffers using changeType. 28083 * 28084 * @param {Object} codecs 28085 * Codecs to switch to 28086 */ 28087 28088 28089 addOrChangeSourceBuffers(codecs) { 28090 if (!codecs || typeof codecs !== 'object' || Object.keys(codecs).length === 0) { 28091 throw new Error('Cannot addOrChangeSourceBuffers to undefined codecs'); 28092 } 28093 28094 Object.keys(codecs).forEach(type => { 28095 const codec = codecs[type]; 28096 28097 if (!this.hasCreatedSourceBuffers()) { 28098 return this.addSourceBuffer(type, codec); 28099 } 28100 28101 if (this.canChangeType()) { 28102 this.changeType(type, codec); 28103 } 28104 }); 28105 } 28106 /** 28107 * Queue an update to append an ArrayBuffer. 28108 * 28109 * @param {MediaObject} object containing audioBytes and/or videoBytes 28110 * @param {Function} done the function to call when done 28111 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 28112 */ 28113 28114 28115 appendBuffer(options, doneFn) { 28116 const { 28117 segmentInfo, 28118 type, 28119 bytes 28120 } = options; 28121 this.processedAppend_ = true; 28122 28123 if (type === 'audio' && this.videoBuffer && !this.videoAppendQueued_) { 28124 this.delayedAudioAppendQueue_.push([options, doneFn]); 28125 this.logger_(`delayed audio append of ${bytes.length} until video append`); 28126 return; 28127 } // In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will 28128 // not be fired. This means that the queue will be blocked until the next action 28129 // taken by the segment-loader. Provide a mechanism for segment-loader to handle 28130 // these errors by calling the doneFn with the specific error. 28131 28132 28133 const onError = doneFn; 28134 pushQueue({ 28135 type, 28136 sourceUpdater: this, 28137 action: actions.appendBuffer(bytes, segmentInfo || { 28138 mediaIndex: -1 28139 }, onError), 28140 doneFn, 28141 name: 'appendBuffer' 28142 }); 28143 28144 if (type === 'video') { 28145 this.videoAppendQueued_ = true; 28146 28147 if (!this.delayedAudioAppendQueue_.length) { 28148 return; 28149 } 28150 28151 const queue = this.delayedAudioAppendQueue_.slice(); 28152 this.logger_(`queuing delayed audio ${queue.length} appendBuffers`); 28153 this.delayedAudioAppendQueue_.length = 0; 28154 queue.forEach(que => { 28155 this.appendBuffer.apply(this, que); 28156 }); 28157 } 28158 } 28159 /** 28160 * Get the audio buffer's buffered timerange. 28161 * 28162 * @return {TimeRange} 28163 * The audio buffer's buffered time range 28164 */ 28165 28166 28167 audioBuffered() { 28168 // no media source/source buffer or it isn't in the media sources 28169 // source buffer list 28170 if (!inSourceBuffers(this.mediaSource, this.audioBuffer)) { 28171 return createTimeRanges(); 28172 } 28173 28174 return this.audioBuffer.buffered ? this.audioBuffer.buffered : createTimeRanges(); 28175 } 28176 /** 28177 * Get the video buffer's buffered timerange. 28178 * 28179 * @return {TimeRange} 28180 * The video buffer's buffered time range 28181 */ 28182 28183 28184 videoBuffered() { 28185 // no media source/source buffer or it isn't in the media sources 28186 // source buffer list 28187 if (!inSourceBuffers(this.mediaSource, this.videoBuffer)) { 28188 return createTimeRanges(); 28189 } 28190 28191 return this.videoBuffer.buffered ? this.videoBuffer.buffered : createTimeRanges(); 28192 } 28193 /** 28194 * Get a combined video/audio buffer's buffered timerange. 28195 * 28196 * @return {TimeRange} 28197 * the combined time range 28198 */ 28199 28200 28201 buffered() { 28202 const video = inSourceBuffers(this.mediaSource, this.videoBuffer) ? this.videoBuffer : null; 28203 const audio = inSourceBuffers(this.mediaSource, this.audioBuffer) ? this.audioBuffer : null; 28204 28205 if (audio && !video) { 28206 return this.audioBuffered(); 28207 } 28208 28209 if (video && !audio) { 28210 return this.videoBuffered(); 28211 } 28212 28213 return bufferIntersection(this.audioBuffered(), this.videoBuffered()); 28214 } 28215 /** 28216 * Add a callback to the queue that will set duration on the mediaSource. 28217 * 28218 * @param {number} duration 28219 * The duration to set 28220 * 28221 * @param {Function} [doneFn] 28222 * function to run after duration has been set. 28223 */ 28224 28225 28226 setDuration(duration, doneFn = noop) { 28227 // In order to set the duration on the media source, it's necessary to wait for all 28228 // source buffers to no longer be updating. "If the updating attribute equals true on 28229 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 28230 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 28231 pushQueue({ 28232 type: 'mediaSource', 28233 sourceUpdater: this, 28234 action: actions.duration(duration), 28235 name: 'duration', 28236 doneFn 28237 }); 28238 } 28239 /** 28240 * Add a mediaSource endOfStream call to the queue 28241 * 28242 * @param {Error} [error] 28243 * Call endOfStream with an error 28244 * 28245 * @param {Function} [doneFn] 28246 * A function that should be called when the 28247 * endOfStream call has finished. 28248 */ 28249 28250 28251 endOfStream(error = null, doneFn = noop) { 28252 if (typeof error !== 'string') { 28253 error = undefined; 28254 } // In order to set the duration on the media source, it's necessary to wait for all 28255 // source buffers to no longer be updating. "If the updating attribute equals true on 28256 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 28257 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 28258 28259 28260 pushQueue({ 28261 type: 'mediaSource', 28262 sourceUpdater: this, 28263 action: actions.endOfStream(error), 28264 name: 'endOfStream', 28265 doneFn 28266 }); 28267 } 28268 /** 28269 * Queue an update to remove a time range from the buffer. 28270 * 28271 * @param {number} start where to start the removal 28272 * @param {number} end where to end the removal 28273 * @param {Function} [done=noop] optional callback to be executed when the remove 28274 * operation is complete 28275 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 28276 */ 28277 28278 28279 removeAudio(start, end, done = noop) { 28280 if (!this.audioBuffered().length || this.audioBuffered().end(0) === 0) { 28281 done(); 28282 return; 28283 } 28284 28285 pushQueue({ 28286 type: 'audio', 28287 sourceUpdater: this, 28288 action: actions.remove(start, end), 28289 doneFn: done, 28290 name: 'remove' 28291 }); 28292 } 28293 /** 28294 * Queue an update to remove a time range from the buffer. 28295 * 28296 * @param {number} start where to start the removal 28297 * @param {number} end where to end the removal 28298 * @param {Function} [done=noop] optional callback to be executed when the remove 28299 * operation is complete 28300 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 28301 */ 28302 28303 28304 removeVideo(start, end, done = noop) { 28305 if (!this.videoBuffered().length || this.videoBuffered().end(0) === 0) { 28306 done(); 28307 return; 28308 } 28309 28310 pushQueue({ 28311 type: 'video', 28312 sourceUpdater: this, 28313 action: actions.remove(start, end), 28314 doneFn: done, 28315 name: 'remove' 28316 }); 28317 } 28318 /** 28319 * Whether the underlying sourceBuffer is updating or not 28320 * 28321 * @return {boolean} the updating status of the SourceBuffer 28322 */ 28323 28324 28325 updating() { 28326 // the audio/video source buffer is updating 28327 if (updating('audio', this) || updating('video', this)) { 28328 return true; 28329 } 28330 28331 return false; 28332 } 28333 /** 28334 * Set/get the timestampoffset on the audio SourceBuffer 28335 * 28336 * @return {number} the timestamp offset 28337 */ 28338 28339 28340 audioTimestampOffset(offset) { 28341 if (typeof offset !== 'undefined' && this.audioBuffer && // no point in updating if it's the same 28342 this.audioTimestampOffset_ !== offset) { 28343 pushQueue({ 28344 type: 'audio', 28345 sourceUpdater: this, 28346 action: actions.timestampOffset(offset), 28347 name: 'timestampOffset' 28348 }); 28349 this.audioTimestampOffset_ = offset; 28350 } 28351 28352 return this.audioTimestampOffset_; 28353 } 28354 /** 28355 * Set/get the timestampoffset on the video SourceBuffer 28356 * 28357 * @return {number} the timestamp offset 28358 */ 28359 28360 28361 videoTimestampOffset(offset) { 28362 if (typeof offset !== 'undefined' && this.videoBuffer && // no point in updating if it's the same 28363 this.videoTimestampOffset_ !== offset) { 28364 pushQueue({ 28365 type: 'video', 28366 sourceUpdater: this, 28367 action: actions.timestampOffset(offset), 28368 name: 'timestampOffset' 28369 }); 28370 this.videoTimestampOffset_ = offset; 28371 } 28372 28373 return this.videoTimestampOffset_; 28374 } 28375 /** 28376 * Add a function to the queue that will be called 28377 * when it is its turn to run in the audio queue. 28378 * 28379 * @param {Function} callback 28380 * The callback to queue. 28381 */ 28382 28383 28384 audioQueueCallback(callback) { 28385 if (!this.audioBuffer) { 28386 return; 28387 } 28388 28389 pushQueue({ 28390 type: 'audio', 28391 sourceUpdater: this, 28392 action: actions.callback(callback), 28393 name: 'callback' 28394 }); 28395 } 28396 /** 28397 * Add a function to the queue that will be called 28398 * when it is its turn to run in the video queue. 28399 * 28400 * @param {Function} callback 28401 * The callback to queue. 28402 */ 28403 28404 28405 videoQueueCallback(callback) { 28406 if (!this.videoBuffer) { 28407 return; 28408 } 28409 28410 pushQueue({ 28411 type: 'video', 28412 sourceUpdater: this, 28413 action: actions.callback(callback), 28414 name: 'callback' 28415 }); 28416 } 28417 /** 28418 * dispose of the source updater and the underlying sourceBuffer 28419 */ 28420 28421 28422 dispose() { 28423 this.trigger('dispose'); 28424 bufferTypes.forEach(type => { 28425 this.abort(type); 28426 28427 if (this.canRemoveSourceBuffer()) { 28428 this.removeSourceBuffer(type); 28429 } else { 28430 this[`${type}QueueCallback`](() => cleanupBuffer(type, this)); 28431 } 28432 }); 28433 this.videoAppendQueued_ = false; 28434 this.delayedAudioAppendQueue_.length = 0; 28435 28436 if (this.sourceopenListener_) { 28437 this.mediaSource.removeEventListener('sourceopen', this.sourceopenListener_); 28438 } 28439 28440 this.off(); 28441 } 28442 28443 } 28444 28445 const uint8ToUtf8 = uintArray => decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray))); 28446 const bufferToHexString = buffer => { 28447 const uInt8Buffer = new Uint8Array(buffer); 28448 return Array.from(uInt8Buffer).map(byte => byte.toString(16).padStart(2, '0')).join(''); 28449 }; 28450 28451 /** 28452 * @file vtt-segment-loader.js 28453 */ 28454 const VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0))); 28455 28456 class NoVttJsError extends Error { 28457 constructor() { 28458 super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.'); 28459 } 28460 28461 } 28462 /** 28463 * An object that manages segment loading and appending. 28464 * 28465 * @class VTTSegmentLoader 28466 * @param {Object} options required and optional options 28467 * @extends videojs.EventTarget 28468 */ 28469 28470 28471 class VTTSegmentLoader extends SegmentLoader { 28472 constructor(settings, options = {}) { 28473 super(settings, options); // SegmentLoader requires a MediaSource be specified or it will throw an error; 28474 // however, VTTSegmentLoader has no need of a media source, so delete the reference 28475 28476 this.mediaSource_ = null; 28477 this.subtitlesTrack_ = null; 28478 this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks; 28479 this.loadVttJs = settings.loadVttJs; // The VTT segment will have its own time mappings. Saving VTT segment timing info in 28480 // the sync controller leads to improper behavior. 28481 28482 this.shouldSaveSegmentTimingInfo_ = false; 28483 } 28484 /** 28485 * Indicates which time ranges are buffered 28486 * 28487 * @return {TimeRange} 28488 * TimeRange object representing the current buffered ranges 28489 */ 28490 28491 28492 buffered_() { 28493 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) { 28494 return createTimeRanges(); 28495 } 28496 28497 const cues = this.subtitlesTrack_.cues; 28498 const start = cues[0].startTime; 28499 const end = cues[cues.length - 1].startTime; 28500 return createTimeRanges([[start, end]]); 28501 } 28502 /** 28503 * Gets and sets init segment for the provided map 28504 * 28505 * @param {Object} map 28506 * The map object representing the init segment to get or set 28507 * @param {boolean=} set 28508 * If true, the init segment for the provided map should be saved 28509 * @return {Object} 28510 * map object for desired init segment 28511 */ 28512 28513 28514 initSegmentForMap(map, set = false) { 28515 if (!map) { 28516 return null; 28517 } 28518 28519 const id = initSegmentId(map); 28520 let storedMap = this.initSegments_[id]; 28521 28522 if (set && !storedMap && map.bytes) { 28523 // append WebVTT line terminators to the media initialization segment if it exists 28524 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 28525 // requires two or more WebVTT line terminators between the WebVTT header and the 28526 // rest of the file 28527 const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 28528 const combinedSegment = new Uint8Array(combinedByteLength); 28529 combinedSegment.set(map.bytes); 28530 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 28531 this.initSegments_[id] = storedMap = { 28532 resolvedUri: map.resolvedUri, 28533 byterange: map.byterange, 28534 bytes: combinedSegment 28535 }; 28536 } 28537 28538 return storedMap || map; 28539 } 28540 /** 28541 * Returns true if all configuration required for loading is present, otherwise false. 28542 * 28543 * @return {boolean} True if the all configuration is ready for loading 28544 * @private 28545 */ 28546 28547 28548 couldBeginLoading_() { 28549 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 28550 } 28551 /** 28552 * Once all the starting parameters have been specified, begin 28553 * operation. This method should only be invoked from the INIT 28554 * state. 28555 * 28556 * @private 28557 */ 28558 28559 28560 init_() { 28561 this.state = 'READY'; 28562 this.resetEverything(); 28563 return this.monitorBuffer_(); 28564 } 28565 /** 28566 * Set a subtitle track on the segment loader to add subtitles to 28567 * 28568 * @param {TextTrack=} track 28569 * The text track to add loaded subtitles to 28570 * @return {TextTrack} 28571 * Returns the subtitles track 28572 */ 28573 28574 28575 track(track) { 28576 if (typeof track === 'undefined') { 28577 return this.subtitlesTrack_; 28578 } 28579 28580 this.subtitlesTrack_ = track; // if we were unpaused but waiting for a sourceUpdater, start 28581 // buffering now 28582 28583 if (this.state === 'INIT' && this.couldBeginLoading_()) { 28584 this.init_(); 28585 } 28586 28587 return this.subtitlesTrack_; 28588 } 28589 /** 28590 * Remove any data in the source buffer between start and end times 28591 * 28592 * @param {number} start - the start time of the region to remove from the buffer 28593 * @param {number} end - the end time of the region to remove from the buffer 28594 */ 28595 28596 28597 remove(start, end) { 28598 removeCuesFromTrack(start, end, this.subtitlesTrack_); 28599 } 28600 /** 28601 * fill the buffer with segements unless the sourceBuffers are 28602 * currently updating 28603 * 28604 * Note: this function should only ever be called by monitorBuffer_ 28605 * and never directly 28606 * 28607 * @private 28608 */ 28609 28610 28611 fillBuffer_() { 28612 // see if we need to begin loading immediately 28613 const segmentInfo = this.chooseNextRequest_(); 28614 28615 if (!segmentInfo) { 28616 return; 28617 } 28618 28619 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 28620 // We don't have the timestamp offset that we need to sync subtitles. 28621 // Rerun on a timestamp offset or user interaction. 28622 const checkTimestampOffset = () => { 28623 this.state = 'READY'; 28624 28625 if (!this.paused()) { 28626 // if not paused, queue a buffer check as soon as possible 28627 this.monitorBuffer_(); 28628 } 28629 }; 28630 28631 this.syncController_.one('timestampoffset', checkTimestampOffset); 28632 this.state = 'WAITING_ON_TIMELINE'; 28633 return; 28634 } 28635 28636 this.loadSegment_(segmentInfo); 28637 } // never set a timestamp offset for vtt segments. 28638 28639 28640 timestampOffsetForSegment_() { 28641 return null; 28642 } 28643 28644 chooseNextRequest_() { 28645 return this.skipEmptySegments_(super.chooseNextRequest_()); 28646 } 28647 /** 28648 * Prevents the segment loader from requesting segments we know contain no subtitles 28649 * by walking forward until we find the next segment that we don't know whether it is 28650 * empty or not. 28651 * 28652 * @param {Object} segmentInfo 28653 * a segment info object that describes the current segment 28654 * @return {Object} 28655 * a segment info object that describes the current segment 28656 */ 28657 28658 28659 skipEmptySegments_(segmentInfo) { 28660 while (segmentInfo && segmentInfo.segment.empty) { 28661 // stop at the last possible segmentInfo 28662 if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) { 28663 segmentInfo = null; 28664 break; 28665 } 28666 28667 segmentInfo = this.generateSegmentInfo_({ 28668 playlist: segmentInfo.playlist, 28669 mediaIndex: segmentInfo.mediaIndex + 1, 28670 startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration, 28671 isSyncRequest: segmentInfo.isSyncRequest 28672 }); 28673 } 28674 28675 return segmentInfo; 28676 } 28677 28678 stopForError(error) { 28679 this.error(error); 28680 this.state = 'READY'; 28681 this.pause(); 28682 this.trigger('error'); 28683 } 28684 /** 28685 * append a decrypted segement to the SourceBuffer through a SourceUpdater 28686 * 28687 * @private 28688 */ 28689 28690 28691 segmentRequestFinished_(error, simpleSegment, result) { 28692 if (!this.subtitlesTrack_) { 28693 this.state = 'READY'; 28694 return; 28695 } 28696 28697 this.saveTransferStats_(simpleSegment.stats); // the request was aborted 28698 28699 if (!this.pendingSegment_) { 28700 this.state = 'READY'; 28701 this.mediaRequestsAborted += 1; 28702 return; 28703 } 28704 28705 if (error) { 28706 if (error.code === REQUEST_ERRORS.TIMEOUT) { 28707 this.handleTimeout_(); 28708 } 28709 28710 if (error.code === REQUEST_ERRORS.ABORTED) { 28711 this.mediaRequestsAborted += 1; 28712 } else { 28713 this.mediaRequestsErrored += 1; 28714 } 28715 28716 this.stopForError(error); 28717 return; 28718 } 28719 28720 const segmentInfo = this.pendingSegment_; 28721 const isMp4WebVttSegmentWithCues = result.mp4VttCues && result.mp4VttCues.length; 28722 28723 if (isMp4WebVttSegmentWithCues) { 28724 segmentInfo.mp4VttCues = result.mp4VttCues; 28725 } // although the VTT segment loader bandwidth isn't really used, it's good to 28726 // maintain functionality between segment loaders 28727 28728 28729 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); // if this request included a segment key, save that data in the cache 28730 28731 if (simpleSegment.key) { 28732 this.segmentKey(simpleSegment.key, true); 28733 } 28734 28735 this.state = 'APPENDING'; // used for tests 28736 28737 this.trigger('appending'); 28738 const segment = segmentInfo.segment; 28739 28740 if (segment.map) { 28741 segment.map.bytes = simpleSegment.map.bytes; 28742 } 28743 28744 segmentInfo.bytes = simpleSegment.bytes; // Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading 28745 28746 if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') { 28747 this.state = 'WAITING_ON_VTTJS'; // should be fine to call multiple times 28748 // script will be loaded once but multiple listeners will be added to the queue, which is expected. 28749 28750 this.loadVttJs().then(() => this.segmentRequestFinished_(error, simpleSegment, result), () => this.stopForError({ 28751 message: 'Error loading vtt.js' 28752 })); 28753 return; 28754 } 28755 28756 segment.requested = true; 28757 28758 try { 28759 this.parseVTTCues_(segmentInfo); 28760 } catch (e) { 28761 this.stopForError({ 28762 message: e.message, 28763 metadata: { 28764 errorType: videojs__default["default"].Error.StreamingVttParserError, 28765 error: e 28766 } 28767 }); 28768 return; 28769 } 28770 28771 if (!isMp4WebVttSegmentWithCues) { 28772 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_); 28773 } 28774 28775 if (segmentInfo.cues.length) { 28776 segmentInfo.timingInfo = { 28777 start: segmentInfo.cues[0].startTime, 28778 end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime 28779 }; 28780 } else { 28781 segmentInfo.timingInfo = { 28782 start: segmentInfo.startOfSegment, 28783 end: segmentInfo.startOfSegment + segmentInfo.duration 28784 }; 28785 } 28786 28787 if (segmentInfo.isSyncRequest) { 28788 this.trigger('syncinfoupdate'); 28789 this.pendingSegment_ = null; 28790 this.state = 'READY'; 28791 return; 28792 } 28793 28794 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 28795 this.mediaSecondsLoaded += segment.duration; // Create VTTCue instances for each cue in the new segment and add them to 28796 // the subtitle track 28797 28798 segmentInfo.cues.forEach(cue => { 28799 this.subtitlesTrack_.addCue(this.featuresNativeTextTracks_ ? new window.VTTCue(cue.startTime, cue.endTime, cue.text) : cue); 28800 }); // Remove any duplicate cues from the subtitle track. The WebVTT spec allows 28801 // cues to have identical time-intervals, but if the text is also identical 28802 // we can safely assume it is a duplicate that can be removed (ex. when a cue 28803 // "overlaps" VTT segments) 28804 28805 removeDuplicateCuesFromTrack(this.subtitlesTrack_); 28806 this.handleAppendsDone_(); 28807 } 28808 28809 handleData_(simpleSegment, result) { 28810 const isVttType = simpleSegment && simpleSegment.type === 'vtt'; 28811 const isTextResult = result && result.type === 'text'; 28812 const isFmp4VttSegment = isVttType && isTextResult; // handle segment data for fmp4 encapsulated webvtt 28813 28814 if (isFmp4VttSegment) { 28815 super.handleData_(simpleSegment, result); 28816 } 28817 } 28818 28819 updateTimingInfoEnd_() {// noop 28820 } 28821 /** 28822 * Utility function for converting mp4 webvtt cue objects into VTTCues. 28823 * 28824 * @param {Object} segmentInfo with mp4 webvtt cues for parsing into VTTCue objecs 28825 */ 28826 28827 28828 parseMp4VttCues_(segmentInfo) { 28829 const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset(); 28830 segmentInfo.mp4VttCues.forEach(cue => { 28831 const start = cue.start + timestampOffset; 28832 const end = cue.end + timestampOffset; 28833 const vttCue = new window.VTTCue(start, end, cue.cueText); 28834 28835 if (cue.settings) { 28836 cue.settings.split(' ').forEach(cueSetting => { 28837 const keyValString = cueSetting.split(':'); 28838 const key = keyValString[0]; 28839 const value = keyValString[1]; 28840 vttCue[key] = isNaN(value) ? value : Number(value); 28841 }); 28842 } 28843 28844 segmentInfo.cues.push(vttCue); 28845 }); 28846 } 28847 /** 28848 * Uses the WebVTT parser to parse the segment response 28849 * 28850 * @throws NoVttJsError 28851 * 28852 * @param {Object} segmentInfo 28853 * a segment info object that describes the current segment 28854 * @private 28855 */ 28856 28857 28858 parseVTTCues_(segmentInfo) { 28859 let decoder; 28860 let decodeBytesToString = false; 28861 28862 if (typeof window.WebVTT !== 'function') { 28863 // caller is responsible for exception handling. 28864 throw new NoVttJsError(); 28865 } 28866 28867 segmentInfo.cues = []; 28868 segmentInfo.timestampmap = { 28869 MPEGTS: 0, 28870 LOCAL: 0 28871 }; 28872 28873 if (segmentInfo.mp4VttCues) { 28874 this.parseMp4VttCues_(segmentInfo); 28875 return; 28876 } 28877 28878 if (typeof window.TextDecoder === 'function') { 28879 decoder = new window.TextDecoder('utf8'); 28880 } else { 28881 decoder = window.WebVTT.StringDecoder(); 28882 decodeBytesToString = true; 28883 } 28884 28885 const parser = new window.WebVTT.Parser(window, window.vttjs, decoder); 28886 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 28887 28888 parser.ontimestampmap = map => { 28889 segmentInfo.timestampmap = map; 28890 }; 28891 28892 parser.onparsingerror = error => { 28893 videojs__default["default"].log.warn('Error encountered when parsing cues: ' + error.message); 28894 }; 28895 28896 if (segmentInfo.segment.map) { 28897 let mapData = segmentInfo.segment.map.bytes; 28898 28899 if (decodeBytesToString) { 28900 mapData = uint8ToUtf8(mapData); 28901 } 28902 28903 parser.parse(mapData); 28904 } 28905 28906 let segmentData = segmentInfo.bytes; 28907 28908 if (decodeBytesToString) { 28909 segmentData = uint8ToUtf8(segmentData); 28910 } 28911 28912 parser.parse(segmentData); 28913 parser.flush(); 28914 } 28915 /** 28916 * Updates the start and end times of any cues parsed by the WebVTT parser using 28917 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 28918 * from the SyncController 28919 * 28920 * @param {Object} segmentInfo 28921 * a segment info object that describes the current segment 28922 * @param {Object} mappingObj 28923 * object containing a mapping from TS to media time 28924 * @param {Object} playlist 28925 * the playlist object containing the segment 28926 * @private 28927 */ 28928 28929 28930 updateTimeMapping_(segmentInfo, mappingObj, playlist) { 28931 const segment = segmentInfo.segment; 28932 28933 if (!mappingObj) { 28934 // If the sync controller does not have a mapping of TS to Media Time for the 28935 // timeline, then we don't have enough information to update the cue 28936 // start/end times 28937 return; 28938 } 28939 28940 if (!segmentInfo.cues.length) { 28941 // If there are no cues, we also do not have enough information to figure out 28942 // segment timing. Mark that the segment contains no cues so we don't re-request 28943 // an empty segment. 28944 segment.empty = true; 28945 return; 28946 } 28947 28948 const { 28949 MPEGTS, 28950 LOCAL 28951 } = segmentInfo.timestampmap; 28952 /** 28953 * From the spec: 28954 * The MPEGTS media timestamp MUST use a 90KHz timescale, 28955 * even when non-WebVTT Media Segments use a different timescale. 28956 */ 28957 28958 const mpegTsInSeconds = MPEGTS / clock.ONE_SECOND_IN_TS; 28959 const diff = mpegTsInSeconds - LOCAL + mappingObj.mapping; 28960 segmentInfo.cues.forEach(cue => { 28961 const duration = cue.endTime - cue.startTime; 28962 const startTime = this.handleRollover_(cue.startTime + diff, mappingObj.time); 28963 cue.startTime = Math.max(startTime, 0); 28964 cue.endTime = Math.max(startTime + duration, 0); 28965 }); 28966 28967 if (!playlist.syncInfo) { 28968 const firstStart = segmentInfo.cues[0].startTime; 28969 const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 28970 playlist.syncInfo = { 28971 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 28972 time: Math.min(firstStart, lastStart - segment.duration) 28973 }; 28974 } 28975 } 28976 /** 28977 * MPEG-TS PES timestamps are limited to 2^33. 28978 * Once they reach 2^33, they roll over to 0. 28979 * mux.js handles PES timestamp rollover for the following scenarios: 28980 * [forward rollover(right)] -> 28981 * PES timestamps monotonically increase, and once they reach 2^33, they roll over to 0 28982 * [backward rollover(left)] --> 28983 * we seek back to position before rollover. 28984 * 28985 * According to the HLS SPEC: 28986 * When synchronizing WebVTT with PES timestamps, clients SHOULD account 28987 * for cases where the 33-bit PES timestamps have wrapped and the WebVTT 28988 * cue times have not. When the PES timestamp wraps, the WebVTT Segment 28989 * SHOULD have a X-TIMESTAMP-MAP header that maps the current WebVTT 28990 * time to the new (low valued) PES timestamp. 28991 * 28992 * So we want to handle rollover here and align VTT Cue start/end time to the player's time. 28993 */ 28994 28995 28996 handleRollover_(value, reference) { 28997 if (reference === null) { 28998 return value; 28999 } 29000 29001 let valueIn90khz = value * clock.ONE_SECOND_IN_TS; 29002 const referenceIn90khz = reference * clock.ONE_SECOND_IN_TS; 29003 let offset; 29004 29005 if (referenceIn90khz < valueIn90khz) { 29006 // - 2^33 29007 offset = -8589934592; 29008 } else { 29009 // + 2^33 29010 offset = 8589934592; 29011 } // distance(value - reference) > 2^32 29012 29013 29014 while (Math.abs(valueIn90khz - referenceIn90khz) > 4294967296) { 29015 valueIn90khz += offset; 29016 } 29017 29018 return valueIn90khz / clock.ONE_SECOND_IN_TS; 29019 } 29020 29021 } 29022 29023 /** 29024 * @file ad-cue-tags.js 29025 */ 29026 29027 /** 29028 * Searches for an ad cue that overlaps with the given mediaTime 29029 * 29030 * @param {Object} track 29031 * the track to find the cue for 29032 * 29033 * @param {number} mediaTime 29034 * the time to find the cue at 29035 * 29036 * @return {Object|null} 29037 * the found cue or null 29038 */ 29039 const findAdCue = function (track, mediaTime) { 29040 const cues = track.cues; 29041 29042 for (let i = 0; i < cues.length; i++) { 29043 const cue = cues[i]; 29044 29045 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 29046 return cue; 29047 } 29048 } 29049 29050 return null; 29051 }; 29052 const updateAdCues = function (media, track, offset = 0) { 29053 if (!media.segments) { 29054 return; 29055 } 29056 29057 let mediaTime = offset; 29058 let cue; 29059 29060 for (let i = 0; i < media.segments.length; i++) { 29061 const segment = media.segments[i]; 29062 29063 if (!cue) { 29064 // Since the cues will span for at least the segment duration, adding a fudge 29065 // factor of half segment duration will prevent duplicate cues from being 29066 // created when timing info is not exact (e.g. cue start time initialized 29067 // at 10.006677, but next call mediaTime is 10.003332 ) 29068 cue = findAdCue(track, mediaTime + segment.duration / 2); 29069 } 29070 29071 if (cue) { 29072 if ('cueIn' in segment) { 29073 // Found a CUE-IN so end the cue 29074 cue.endTime = mediaTime; 29075 cue.adEndTime = mediaTime; 29076 mediaTime += segment.duration; 29077 cue = null; 29078 continue; 29079 } 29080 29081 if (mediaTime < cue.endTime) { 29082 // Already processed this mediaTime for this cue 29083 mediaTime += segment.duration; 29084 continue; 29085 } // otherwise extend cue until a CUE-IN is found 29086 29087 29088 cue.endTime += segment.duration; 29089 } else { 29090 if ('cueOut' in segment) { 29091 cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); 29092 cue.adStartTime = mediaTime; // Assumes tag format to be 29093 // #EXT-X-CUE-OUT:30 29094 29095 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 29096 track.addCue(cue); 29097 } 29098 29099 if ('cueOutCont' in segment) { 29100 // Entered into the middle of an ad cue 29101 // Assumes tag formate to be 29102 // #EXT-X-CUE-OUT-CONT:10/30 29103 const [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat); 29104 cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, ''); 29105 cue.adStartTime = mediaTime - adOffset; 29106 cue.adEndTime = cue.adStartTime + adTotal; 29107 track.addCue(cue); 29108 } 29109 } 29110 29111 mediaTime += segment.duration; 29112 } 29113 }; 29114 29115 class SyncInfo { 29116 /** 29117 * @param {number} start - media sequence start 29118 * @param {number} end - media sequence end 29119 * @param {number} segmentIndex - index for associated segment 29120 * @param {number|null} [partIndex] - index for associated part 29121 * @param {boolean} [appended] - appended indicator 29122 * 29123 */ 29124 constructor({ 29125 start, 29126 end, 29127 segmentIndex, 29128 partIndex = null, 29129 appended = false 29130 }) { 29131 this.start_ = start; 29132 this.end_ = end; 29133 this.segmentIndex_ = segmentIndex; 29134 this.partIndex_ = partIndex; 29135 this.appended_ = appended; 29136 } 29137 29138 isInRange(targetTime) { 29139 return targetTime >= this.start && targetTime < this.end; 29140 } 29141 29142 markAppended() { 29143 this.appended_ = true; 29144 } 29145 29146 resetAppendedStatus() { 29147 this.appended_ = false; 29148 } 29149 29150 get isAppended() { 29151 return this.appended_; 29152 } 29153 29154 get start() { 29155 return this.start_; 29156 } 29157 29158 get end() { 29159 return this.end_; 29160 } 29161 29162 get segmentIndex() { 29163 return this.segmentIndex_; 29164 } 29165 29166 get partIndex() { 29167 return this.partIndex_; 29168 } 29169 29170 } 29171 29172 class SyncInfoData { 29173 /** 29174 * 29175 * @param {SyncInfo} segmentSyncInfo - sync info for a given segment 29176 * @param {Array<SyncInfo>} [partsSyncInfo] - sync infos for a list of parts for a given segment 29177 */ 29178 constructor(segmentSyncInfo, partsSyncInfo = []) { 29179 this.segmentSyncInfo_ = segmentSyncInfo; 29180 this.partsSyncInfo_ = partsSyncInfo; 29181 } 29182 29183 get segmentSyncInfo() { 29184 return this.segmentSyncInfo_; 29185 } 29186 29187 get partsSyncInfo() { 29188 return this.partsSyncInfo_; 29189 } 29190 29191 get hasPartsSyncInfo() { 29192 return this.partsSyncInfo_.length > 0; 29193 } 29194 29195 resetAppendStatus() { 29196 this.segmentSyncInfo_.resetAppendedStatus(); 29197 this.partsSyncInfo_.forEach(partSyncInfo => partSyncInfo.resetAppendedStatus()); 29198 } 29199 29200 } 29201 29202 class MediaSequenceSync { 29203 constructor() { 29204 /** 29205 * @type {Map<number, SyncInfoData>} 29206 * @protected 29207 */ 29208 this.storage_ = new Map(); 29209 this.diagnostics_ = ''; 29210 this.isReliable_ = false; 29211 this.start_ = -Infinity; 29212 this.end_ = Infinity; 29213 } 29214 29215 get start() { 29216 return this.start_; 29217 } 29218 29219 get end() { 29220 return this.end_; 29221 } 29222 29223 get diagnostics() { 29224 return this.diagnostics_; 29225 } 29226 29227 get isReliable() { 29228 return this.isReliable_; 29229 } 29230 29231 resetAppendedStatus() { 29232 this.storage_.forEach(syncInfoData => syncInfoData.resetAppendStatus()); 29233 } 29234 /** 29235 * update sync storage 29236 * 29237 * @param {Object} playlist 29238 * @param {number} currentTime 29239 * 29240 * @return {void} 29241 */ 29242 29243 29244 update(playlist, currentTime) { 29245 const { 29246 mediaSequence, 29247 segments 29248 } = playlist; 29249 this.isReliable_ = this.isReliablePlaylist_(mediaSequence, segments); 29250 29251 if (!this.isReliable_) { 29252 return; 29253 } 29254 29255 return this.updateStorage_(segments, mediaSequence, this.calculateBaseTime_(mediaSequence, segments, currentTime)); 29256 } 29257 /** 29258 * @param {number} targetTime 29259 * @return {SyncInfo|null} 29260 */ 29261 29262 29263 getSyncInfoForTime(targetTime) { 29264 for (const { 29265 segmentSyncInfo, 29266 partsSyncInfo 29267 } of this.storage_.values()) { 29268 // Normal segment flow: 29269 if (!partsSyncInfo.length) { 29270 if (segmentSyncInfo.isInRange(targetTime)) { 29271 return segmentSyncInfo; 29272 } 29273 } else { 29274 // Low latency flow: 29275 for (const partSyncInfo of partsSyncInfo) { 29276 if (partSyncInfo.isInRange(targetTime)) { 29277 return partSyncInfo; 29278 } 29279 } 29280 } 29281 } 29282 29283 return null; 29284 } 29285 29286 getSyncInfoForMediaSequence(mediaSequence) { 29287 return this.storage_.get(mediaSequence); 29288 } 29289 29290 updateStorage_(segments, startingMediaSequence, startingTime) { 29291 const newStorage = new Map(); 29292 let newDiagnostics = '\n'; 29293 let currentStart = startingTime; 29294 let currentMediaSequence = startingMediaSequence; 29295 this.start_ = currentStart; 29296 segments.forEach((segment, segmentIndex) => { 29297 const prevSyncInfoData = this.storage_.get(currentMediaSequence); 29298 const segmentStart = currentStart; 29299 const segmentEnd = segmentStart + segment.duration; 29300 const segmentIsAppended = Boolean(prevSyncInfoData && prevSyncInfoData.segmentSyncInfo && prevSyncInfoData.segmentSyncInfo.isAppended); 29301 const segmentSyncInfo = new SyncInfo({ 29302 start: segmentStart, 29303 end: segmentEnd, 29304 appended: segmentIsAppended, 29305 segmentIndex 29306 }); 29307 segment.syncInfo = segmentSyncInfo; 29308 let currentPartStart = currentStart; 29309 const partsSyncInfo = (segment.parts || []).map((part, partIndex) => { 29310 const partStart = currentPartStart; 29311 const partEnd = currentPartStart + part.duration; 29312 const partIsAppended = Boolean(prevSyncInfoData && prevSyncInfoData.partsSyncInfo && prevSyncInfoData.partsSyncInfo[partIndex] && prevSyncInfoData.partsSyncInfo[partIndex].isAppended); 29313 const partSyncInfo = new SyncInfo({ 29314 start: partStart, 29315 end: partEnd, 29316 appended: partIsAppended, 29317 segmentIndex, 29318 partIndex 29319 }); 29320 currentPartStart = partEnd; 29321 newDiagnostics += `Media Sequence: ${currentMediaSequence}.${partIndex} | Range: ${partStart} --> ${partEnd} | Appended: ${partIsAppended}\n`; 29322 part.syncInfo = partSyncInfo; 29323 return partSyncInfo; 29324 }); 29325 newStorage.set(currentMediaSequence, new SyncInfoData(segmentSyncInfo, partsSyncInfo)); 29326 newDiagnostics += `${compactSegmentUrlDescription(segment.resolvedUri)} | Media Sequence: ${currentMediaSequence} | Range: ${segmentStart} --> ${segmentEnd} | Appended: ${segmentIsAppended}\n`; 29327 currentMediaSequence++; 29328 currentStart = segmentEnd; 29329 }); 29330 this.end_ = currentStart; 29331 this.storage_ = newStorage; 29332 this.diagnostics_ = newDiagnostics; 29333 } 29334 29335 calculateBaseTime_(mediaSequence, segments, fallback) { 29336 if (!this.storage_.size) { 29337 // Initial setup flow. 29338 return 0; 29339 } 29340 29341 if (this.storage_.has(mediaSequence)) { 29342 // Normal flow. 29343 return this.storage_.get(mediaSequence).segmentSyncInfo.start; 29344 } 29345 29346 const minMediaSequenceFromStorage = Math.min(...this.storage_.keys()); // This case captures a race condition that can occur if we switch to a new media playlist that is out of date 29347 // and still has an older Media Sequence. If this occurs, we extrapolate backwards to get the base time. 29348 29349 if (mediaSequence < minMediaSequenceFromStorage) { 29350 const mediaSequenceDiff = minMediaSequenceFromStorage - mediaSequence; 29351 let baseTime = this.storage_.get(minMediaSequenceFromStorage).segmentSyncInfo.start; 29352 29353 for (let i = 0; i < mediaSequenceDiff; i++) { 29354 const segment = segments[i]; 29355 baseTime -= segment.duration; 29356 } 29357 29358 return baseTime; 29359 } // Fallback flow. 29360 // There is a gap between last recorded playlist and a new one received. 29361 29362 29363 return fallback; 29364 } 29365 29366 isReliablePlaylist_(mediaSequence, segments) { 29367 return mediaSequence !== undefined && mediaSequence !== null && Array.isArray(segments) && segments.length; 29368 } 29369 29370 } 29371 class DependantMediaSequenceSync extends MediaSequenceSync { 29372 constructor(parent) { 29373 super(); 29374 this.parent_ = parent; 29375 } 29376 29377 calculateBaseTime_(mediaSequence, segments, fallback) { 29378 if (!this.storage_.size) { 29379 const info = this.parent_.getSyncInfoForMediaSequence(mediaSequence); 29380 29381 if (info) { 29382 return info.segmentSyncInfo.start; 29383 } 29384 29385 return 0; 29386 } 29387 29388 return super.calculateBaseTime_(mediaSequence, segments, fallback); 29389 } 29390 29391 } 29392 29393 /** 29394 * @file sync-controller.js 29395 */ 29396 // synchronize expired playlist segments. 29397 // the max media sequence diff is 48 hours of live stream 29398 // content with two second segments. Anything larger than that 29399 // will likely be invalid. 29400 29401 const MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC = 86400; 29402 const syncPointStrategies = [// Stategy "VOD": Handle the VOD-case where the sync-point is *always* 29403 // the equivalence display-time 0 === segment-index 0 29404 { 29405 name: 'VOD', 29406 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 29407 if (duration !== Infinity) { 29408 const syncPoint = { 29409 time: 0, 29410 segmentIndex: 0, 29411 partIndex: null 29412 }; 29413 return syncPoint; 29414 } 29415 29416 return null; 29417 } 29418 }, { 29419 name: 'MediaSequence', 29420 29421 /** 29422 * run media sequence strategy 29423 * 29424 * @param {SyncController} syncController 29425 * @param {Object} playlist 29426 * @param {number} duration 29427 * @param {number} currentTimeline 29428 * @param {number} currentTime 29429 * @param {string} type 29430 */ 29431 run: (syncController, playlist, duration, currentTimeline, currentTime, type) => { 29432 const mediaSequenceSync = syncController.getMediaSequenceSync(type); 29433 29434 if (!mediaSequenceSync) { 29435 return null; 29436 } 29437 29438 if (!mediaSequenceSync.isReliable) { 29439 return null; 29440 } 29441 29442 const syncInfo = mediaSequenceSync.getSyncInfoForTime(currentTime); 29443 29444 if (!syncInfo) { 29445 return null; 29446 } 29447 29448 return { 29449 time: syncInfo.start, 29450 partIndex: syncInfo.partIndex, 29451 segmentIndex: syncInfo.segmentIndex 29452 }; 29453 } 29454 }, // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 29455 { 29456 name: 'ProgramDateTime', 29457 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 29458 if (!Object.keys(syncController.timelineToDatetimeMappings).length) { 29459 return null; 29460 } 29461 29462 let syncPoint = null; 29463 let lastDistance = null; 29464 const partsAndSegments = getPartsAndSegments(playlist); 29465 currentTime = currentTime || 0; 29466 29467 for (let i = 0; i < partsAndSegments.length; i++) { 29468 // start from the end and loop backwards for live 29469 // or start from the front and loop forwards for non-live 29470 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 29471 const partAndSegment = partsAndSegments[index]; 29472 const segment = partAndSegment.segment; 29473 const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline]; 29474 29475 if (!datetimeMapping || !segment.dateTimeObject) { 29476 continue; 29477 } 29478 29479 const segmentTime = segment.dateTimeObject.getTime() / 1000; 29480 let start = segmentTime + datetimeMapping; // take part duration into account. 29481 29482 if (segment.parts && typeof partAndSegment.partIndex === 'number') { 29483 for (let z = 0; z < partAndSegment.partIndex; z++) { 29484 start += segment.parts[z].duration; 29485 } 29486 } 29487 29488 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed 29489 // currentTime and can stop looking for better candidates 29490 29491 if (lastDistance !== null && (distance === 0 || lastDistance < distance)) { 29492 break; 29493 } 29494 29495 lastDistance = distance; 29496 syncPoint = { 29497 time: start, 29498 segmentIndex: partAndSegment.segmentIndex, 29499 partIndex: partAndSegment.partIndex 29500 }; 29501 } 29502 29503 return syncPoint; 29504 } 29505 }, // Stategy "Segment": We have a known time mapping for a timeline and a 29506 // segment in the current timeline with timing data 29507 { 29508 name: 'Segment', 29509 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 29510 let syncPoint = null; 29511 let lastDistance = null; 29512 currentTime = currentTime || 0; 29513 const partsAndSegments = getPartsAndSegments(playlist); 29514 29515 for (let i = 0; i < partsAndSegments.length; i++) { 29516 // start from the end and loop backwards for live 29517 // or start from the front and loop forwards for non-live 29518 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 29519 const partAndSegment = partsAndSegments[index]; 29520 const segment = partAndSegment.segment; 29521 const start = partAndSegment.part && partAndSegment.part.start || segment && segment.start; 29522 29523 if (segment.timeline === currentTimeline && typeof start !== 'undefined') { 29524 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed 29525 // currentTime and can stop looking for better candidates 29526 29527 if (lastDistance !== null && lastDistance < distance) { 29528 break; 29529 } 29530 29531 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 29532 lastDistance = distance; 29533 syncPoint = { 29534 time: start, 29535 segmentIndex: partAndSegment.segmentIndex, 29536 partIndex: partAndSegment.partIndex 29537 }; 29538 } 29539 } 29540 } 29541 29542 return syncPoint; 29543 } 29544 }, // Stategy "Discontinuity": We have a discontinuity with a known 29545 // display-time 29546 { 29547 name: 'Discontinuity', 29548 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 29549 let syncPoint = null; 29550 currentTime = currentTime || 0; 29551 29552 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 29553 let lastDistance = null; 29554 29555 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 29556 const segmentIndex = playlist.discontinuityStarts[i]; 29557 const discontinuity = playlist.discontinuitySequence + i + 1; 29558 const discontinuitySync = syncController.discontinuities[discontinuity]; 29559 29560 if (discontinuitySync) { 29561 const distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed 29562 // currentTime and can stop looking for better candidates 29563 29564 if (lastDistance !== null && lastDistance < distance) { 29565 break; 29566 } 29567 29568 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 29569 lastDistance = distance; 29570 syncPoint = { 29571 time: discontinuitySync.time, 29572 segmentIndex, 29573 partIndex: null 29574 }; 29575 } 29576 } 29577 } 29578 } 29579 29580 return syncPoint; 29581 } 29582 }, // Stategy "Playlist": We have a playlist with a known mapping of 29583 // segment index to display time 29584 { 29585 name: 'Playlist', 29586 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 29587 if (playlist.syncInfo) { 29588 const syncPoint = { 29589 time: playlist.syncInfo.time, 29590 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence, 29591 partIndex: null 29592 }; 29593 return syncPoint; 29594 } 29595 29596 return null; 29597 } 29598 }]; 29599 class SyncController extends videojs__default["default"].EventTarget { 29600 constructor(options = {}) { 29601 super(); // ...for synching across variants 29602 29603 this.timelines = []; 29604 this.discontinuities = []; 29605 this.timelineToDatetimeMappings = {}; // TODO: this map should be only available for HLS. Since only HLS has MediaSequence. 29606 // For some reason this map helps with syncing between quality switch for MPEG-DASH as well. 29607 // Moreover if we disable this map for MPEG-DASH - quality switch will be broken. 29608 // MPEG-DASH should have its own separate sync strategy 29609 29610 const main = new MediaSequenceSync(); 29611 const audio = new DependantMediaSequenceSync(main); 29612 const vtt = new DependantMediaSequenceSync(main); 29613 this.mediaSequenceStorage_ = { 29614 main, 29615 audio, 29616 vtt 29617 }; 29618 this.logger_ = logger('SyncController'); 29619 } 29620 /** 29621 * 29622 * @param {string} loaderType 29623 * @return {MediaSequenceSync|null} 29624 */ 29625 29626 29627 getMediaSequenceSync(loaderType) { 29628 return this.mediaSequenceStorage_[loaderType] || null; 29629 } 29630 /** 29631 * Find a sync-point for the playlist specified 29632 * 29633 * A sync-point is defined as a known mapping from display-time to 29634 * a segment-index in the current playlist. 29635 * 29636 * @param {Playlist} playlist 29637 * The playlist that needs a sync-point 29638 * @param {number} duration 29639 * Duration of the MediaSource (Infinite if playing a live source) 29640 * @param {number} currentTimeline 29641 * The last timeline from which a segment was loaded 29642 * @param {number} currentTime 29643 * Current player's time 29644 * @param {string} type 29645 * Segment loader type 29646 * @return {Object} 29647 * A sync-point object 29648 */ 29649 29650 29651 getSyncPoint(playlist, duration, currentTimeline, currentTime, type) { 29652 // Always use VOD sync point for VOD 29653 if (duration !== Infinity) { 29654 const vodSyncPointStrategy = syncPointStrategies.find(({ 29655 name 29656 }) => name === 'VOD'); 29657 return vodSyncPointStrategy.run(this, playlist, duration); 29658 } 29659 29660 const syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime, type); 29661 29662 if (!syncPoints.length) { 29663 // Signal that we need to attempt to get a sync-point manually 29664 // by fetching a segment in the playlist and constructing 29665 // a sync-point from that information 29666 return null; 29667 } // If we have exact match just return it instead of finding the nearest distance 29668 29669 29670 for (const syncPointInfo of syncPoints) { 29671 const { 29672 syncPoint, 29673 strategy 29674 } = syncPointInfo; 29675 const { 29676 segmentIndex, 29677 time 29678 } = syncPoint; 29679 29680 if (segmentIndex < 0) { 29681 continue; 29682 } 29683 29684 const selectedSegment = playlist.segments[segmentIndex]; 29685 const start = time; 29686 const end = start + selectedSegment.duration; 29687 this.logger_(`Strategy: ${strategy}. Current time: ${currentTime}. selected segment: ${segmentIndex}. Time: [${start} -> ${end}]}`); 29688 29689 if (currentTime >= start && currentTime < end) { 29690 this.logger_('Found sync point with exact match: ', syncPoint); 29691 return syncPoint; 29692 } 29693 } // Now find the sync-point that is closest to the currentTime because 29694 // that should result in the most accurate guess about which segment 29695 // to fetch 29696 29697 29698 return this.selectSyncPoint_(syncPoints, { 29699 key: 'time', 29700 value: currentTime 29701 }); 29702 } 29703 /** 29704 * Calculate the amount of time that has expired off the playlist during playback 29705 * 29706 * @param {Playlist} playlist 29707 * Playlist object to calculate expired from 29708 * @param {number} duration 29709 * Duration of the MediaSource (Infinity if playling a live source) 29710 * @return {number|null} 29711 * The amount of time that has expired off the playlist during playback. Null 29712 * if no sync-points for the playlist can be found. 29713 */ 29714 29715 29716 getExpiredTime(playlist, duration) { 29717 if (!playlist || !playlist.segments) { 29718 return null; 29719 } 29720 29721 const syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0); // Without sync-points, there is not enough information to determine the expired time 29722 29723 if (!syncPoints.length) { 29724 return null; 29725 } 29726 29727 const syncPoint = this.selectSyncPoint_(syncPoints, { 29728 key: 'segmentIndex', 29729 value: 0 29730 }); // If the sync-point is beyond the start of the playlist, we want to subtract the 29731 // duration from index 0 to syncPoint.segmentIndex instead of adding. 29732 29733 if (syncPoint.segmentIndex > 0) { 29734 syncPoint.time *= -1; 29735 } 29736 29737 return Math.abs(syncPoint.time + sumDurations({ 29738 defaultDuration: playlist.targetDuration, 29739 durationList: playlist.segments, 29740 startIndex: syncPoint.segmentIndex, 29741 endIndex: 0 29742 })); 29743 } 29744 /** 29745 * Runs each sync-point strategy and returns a list of sync-points returned by the 29746 * strategies 29747 * 29748 * @private 29749 * @param {Playlist} playlist 29750 * The playlist that needs a sync-point 29751 * @param {number} duration 29752 * Duration of the MediaSource (Infinity if playing a live source) 29753 * @param {number} currentTimeline 29754 * The last timeline from which a segment was loaded 29755 * @param {number} currentTime 29756 * Current player's time 29757 * @param {string} type 29758 * Segment loader type 29759 * @return {Array} 29760 * A list of sync-point objects 29761 */ 29762 29763 29764 runStrategies_(playlist, duration, currentTimeline, currentTime, type) { 29765 const syncPoints = []; // Try to find a sync-point in by utilizing various strategies... 29766 29767 for (let i = 0; i < syncPointStrategies.length; i++) { 29768 const strategy = syncPointStrategies[i]; 29769 const syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime, type); 29770 29771 if (syncPoint) { 29772 syncPoint.strategy = strategy.name; 29773 syncPoints.push({ 29774 strategy: strategy.name, 29775 syncPoint 29776 }); 29777 } 29778 } 29779 29780 return syncPoints; 29781 } 29782 /** 29783 * Selects the sync-point nearest the specified target 29784 * 29785 * @private 29786 * @param {Array} syncPoints 29787 * List of sync-points to select from 29788 * @param {Object} target 29789 * Object specifying the property and value we are targeting 29790 * @param {string} target.key 29791 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 29792 * @param {number} target.value 29793 * The value to target for the specified key. 29794 * @return {Object} 29795 * The sync-point nearest the target 29796 */ 29797 29798 29799 selectSyncPoint_(syncPoints, target) { 29800 let bestSyncPoint = syncPoints[0].syncPoint; 29801 let bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 29802 let bestStrategy = syncPoints[0].strategy; 29803 29804 for (let i = 1; i < syncPoints.length; i++) { 29805 const newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 29806 29807 if (newDistance < bestDistance) { 29808 bestDistance = newDistance; 29809 bestSyncPoint = syncPoints[i].syncPoint; 29810 bestStrategy = syncPoints[i].strategy; 29811 } 29812 } 29813 29814 this.logger_(`syncPoint for [${target.key}: ${target.value}] chosen with strategy` + ` [${bestStrategy}]: [time:${bestSyncPoint.time},` + ` segmentIndex:${bestSyncPoint.segmentIndex}` + (typeof bestSyncPoint.partIndex === 'number' ? `,partIndex:${bestSyncPoint.partIndex}` : '') + ']'); 29815 return bestSyncPoint; 29816 } 29817 /** 29818 * Save any meta-data present on the segments when segments leave 29819 * the live window to the playlist to allow for synchronization at the 29820 * playlist level later. 29821 * 29822 * @param {Playlist} oldPlaylist - The previous active playlist 29823 * @param {Playlist} newPlaylist - The updated and most current playlist 29824 */ 29825 29826 29827 saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 29828 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // Ignore large media sequence gaps 29829 29830 if (mediaSequenceDiff > MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC) { 29831 videojs__default["default"].log.warn(`Not saving expired segment info. Media sequence gap ${mediaSequenceDiff} is too large.`); 29832 return; 29833 } // When a segment expires from the playlist and it has a start time 29834 // save that information as a possible sync-point reference in future 29835 29836 29837 for (let i = mediaSequenceDiff - 1; i >= 0; i--) { 29838 const lastRemovedSegment = oldPlaylist.segments[i]; 29839 29840 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 29841 newPlaylist.syncInfo = { 29842 mediaSequence: oldPlaylist.mediaSequence + i, 29843 time: lastRemovedSegment.start 29844 }; 29845 this.logger_(`playlist refresh sync: [time:${newPlaylist.syncInfo.time},` + ` mediaSequence: ${newPlaylist.syncInfo.mediaSequence}]`); 29846 this.trigger('syncinfoupdate'); 29847 break; 29848 } 29849 } 29850 } 29851 /** 29852 * Save the mapping from playlist's ProgramDateTime to display. This should only happen 29853 * before segments start to load. 29854 * 29855 * @param {Playlist} playlist - The currently active playlist 29856 */ 29857 29858 29859 setDateTimeMappingForStart(playlist) { 29860 // It's possible for the playlist to be updated before playback starts, meaning time 29861 // zero is not yet set. If, during these playlist refreshes, a discontinuity is 29862 // crossed, then the old time zero mapping (for the prior timeline) would be retained 29863 // unless the mappings are cleared. 29864 this.timelineToDatetimeMappings = {}; 29865 29866 if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 29867 const firstSegment = playlist.segments[0]; 29868 const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000; 29869 this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp; 29870 } 29871 } 29872 /** 29873 * Calculates and saves timeline mappings, playlist sync info, and segment timing values 29874 * based on the latest timing information. 29875 * 29876 * @param {Object} options 29877 * Options object 29878 * @param {SegmentInfo} options.segmentInfo 29879 * The current active request information 29880 * @param {boolean} options.shouldSaveTimelineMapping 29881 * If there's a timeline change, determines if the timeline mapping should be 29882 * saved for timeline mapping and program date time mappings. 29883 */ 29884 29885 29886 saveSegmentTimingInfo({ 29887 segmentInfo, 29888 shouldSaveTimelineMapping 29889 }) { 29890 const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_(segmentInfo, segmentInfo.timingInfo, shouldSaveTimelineMapping); 29891 const segment = segmentInfo.segment; 29892 29893 if (didCalculateSegmentTimeMapping) { 29894 this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information 29895 // now with segment timing information 29896 29897 if (!segmentInfo.playlist.syncInfo) { 29898 segmentInfo.playlist.syncInfo = { 29899 mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex, 29900 time: segment.start 29901 }; 29902 } 29903 } 29904 29905 const dateTime = segment.dateTimeObject; 29906 29907 if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) { 29908 this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000); 29909 } 29910 } 29911 29912 timestampOffsetForTimeline(timeline) { 29913 if (typeof this.timelines[timeline] === 'undefined') { 29914 return null; 29915 } 29916 29917 return this.timelines[timeline].time; 29918 } 29919 29920 mappingForTimeline(timeline) { 29921 if (typeof this.timelines[timeline] === 'undefined') { 29922 return null; 29923 } 29924 29925 return this.timelines[timeline].mapping; 29926 } 29927 /** 29928 * Use the "media time" for a segment to generate a mapping to "display time" and 29929 * save that display time to the segment. 29930 * 29931 * @private 29932 * @param {SegmentInfo} segmentInfo 29933 * The current active request information 29934 * @param {Object} timingInfo 29935 * The start and end time of the current segment in "media time" 29936 * @param {boolean} shouldSaveTimelineMapping 29937 * If there's a timeline change, determines if the timeline mapping should be 29938 * saved in timelines. 29939 * @return {boolean} 29940 * Returns false if segment time mapping could not be calculated 29941 */ 29942 29943 29944 calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) { 29945 // TODO: remove side effects 29946 const segment = segmentInfo.segment; 29947 const part = segmentInfo.part; 29948 let mappingObj = this.timelines[segmentInfo.timeline]; 29949 let start; 29950 let end; 29951 29952 if (typeof segmentInfo.timestampOffset === 'number') { 29953 mappingObj = { 29954 time: segmentInfo.startOfSegment, 29955 mapping: segmentInfo.startOfSegment - timingInfo.start 29956 }; 29957 29958 if (shouldSaveTimelineMapping) { 29959 this.timelines[segmentInfo.timeline] = mappingObj; 29960 this.trigger('timestampoffset'); 29961 this.logger_(`time mapping for timeline ${segmentInfo.timeline}: ` + `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]`); 29962 } 29963 29964 start = segmentInfo.startOfSegment; 29965 end = timingInfo.end + mappingObj.mapping; 29966 } else if (mappingObj) { 29967 start = timingInfo.start + mappingObj.mapping; 29968 end = timingInfo.end + mappingObj.mapping; 29969 } else { 29970 return false; 29971 } 29972 29973 if (part) { 29974 part.start = start; 29975 part.end = end; 29976 } // If we don't have a segment start yet or the start value we got 29977 // is less than our current segment.start value, save a new start value. 29978 // We have to do this because parts will have segment timing info saved 29979 // multiple times and we want segment start to be the earliest part start 29980 // value for that segment. 29981 29982 29983 if (!segment.start || start < segment.start) { 29984 segment.start = start; 29985 } 29986 29987 segment.end = end; 29988 return true; 29989 } 29990 /** 29991 * Each time we have discontinuity in the playlist, attempt to calculate the location 29992 * in display of the start of the discontinuity and save that. We also save an accuracy 29993 * value so that we save values with the most accuracy (closest to 0.) 29994 * 29995 * @private 29996 * @param {SegmentInfo} segmentInfo - The current active request information 29997 */ 29998 29999 30000 saveDiscontinuitySyncInfo_(segmentInfo) { 30001 const playlist = segmentInfo.playlist; 30002 const segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where 30003 // the start of the range and it's accuracy is 0 (greater accuracy values 30004 // mean more approximation) 30005 30006 if (segment.discontinuity) { 30007 this.discontinuities[segment.timeline] = { 30008 time: segment.start, 30009 accuracy: 0 30010 }; 30011 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 30012 // Search for future discontinuities that we can provide better timing 30013 // information for and save that information for sync purposes 30014 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 30015 const segmentIndex = playlist.discontinuityStarts[i]; 30016 const discontinuity = playlist.discontinuitySequence + i + 1; 30017 const mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 30018 const accuracy = Math.abs(mediaIndexDiff); 30019 30020 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) { 30021 let time; 30022 30023 if (mediaIndexDiff < 0) { 30024 time = segment.start - sumDurations({ 30025 defaultDuration: playlist.targetDuration, 30026 durationList: playlist.segments, 30027 startIndex: segmentInfo.mediaIndex, 30028 endIndex: segmentIndex 30029 }); 30030 } else { 30031 time = segment.end + sumDurations({ 30032 defaultDuration: playlist.targetDuration, 30033 durationList: playlist.segments, 30034 startIndex: segmentInfo.mediaIndex + 1, 30035 endIndex: segmentIndex 30036 }); 30037 } 30038 30039 this.discontinuities[discontinuity] = { 30040 time, 30041 accuracy 30042 }; 30043 } 30044 } 30045 } 30046 } 30047 30048 dispose() { 30049 this.trigger('dispose'); 30050 this.off(); 30051 } 30052 30053 } 30054 30055 /** 30056 * The TimelineChangeController acts as a source for segment loaders to listen for and 30057 * keep track of latest and pending timeline changes. This is useful to ensure proper 30058 * sync, as each loader may need to make a consideration for what timeline the other 30059 * loader is on before making changes which could impact the other loader's media. 30060 * 30061 * @class TimelineChangeController 30062 * @extends videojs.EventTarget 30063 */ 30064 30065 class TimelineChangeController extends videojs__default["default"].EventTarget { 30066 constructor() { 30067 super(); 30068 this.pendingTimelineChanges_ = {}; 30069 this.lastTimelineChanges_ = {}; 30070 } 30071 30072 clearPendingTimelineChange(type) { 30073 this.pendingTimelineChanges_[type] = null; 30074 this.trigger('pendingtimelinechange'); 30075 } 30076 30077 pendingTimelineChange({ 30078 type, 30079 from, 30080 to 30081 }) { 30082 if (typeof from === 'number' && typeof to === 'number') { 30083 this.pendingTimelineChanges_[type] = { 30084 type, 30085 from, 30086 to 30087 }; 30088 this.trigger('pendingtimelinechange'); 30089 } 30090 30091 return this.pendingTimelineChanges_[type]; 30092 } 30093 30094 lastTimelineChange({ 30095 type, 30096 from, 30097 to 30098 }) { 30099 if (typeof from === 'number' && typeof to === 'number') { 30100 this.lastTimelineChanges_[type] = { 30101 type, 30102 from, 30103 to 30104 }; 30105 delete this.pendingTimelineChanges_[type]; 30106 const metadata = { 30107 timelineChangeInfo: { 30108 from, 30109 to 30110 } 30111 }; 30112 this.trigger({ 30113 type: 'timelinechange', 30114 metadata 30115 }); 30116 } 30117 30118 return this.lastTimelineChanges_[type]; 30119 } 30120 30121 dispose() { 30122 this.trigger('dispose'); 30123 this.pendingTimelineChanges_ = {}; 30124 this.lastTimelineChanges_ = {}; 30125 this.off(); 30126 } 30127 30128 } 30129 30130 /* rollup-plugin-worker-factory start for worker!/
30130home/runner/work/http-streaming
vendor: 21,606 bytes, lines 30130-30783
30130/http-streaming/src/decrypter-worker.js */ 30131 const workerCode = transform(getWorkerString(function () { 30132 /** 30133 * @file stream.js 30134 */ 30135 30136 /** 30137 * A lightweight readable stream implemention that handles event dispatching. 30138 * 30139 * @class Stream 30140 */ 30141 30142 var Stream = /*#__PURE__*/function () { 30143 function Stream() { 30144 this.listeners = {}; 30145 } 30146 /** 30147 * Add a listener for a specified event type. 30148 * 30149 * @param {string} type the event name 30150 * @param {Function} listener the callback to be invoked when an event of 30151 * the specified type occurs 30152 */ 30153 30154 30155 var _proto = Stream.prototype; 30156 30157 _proto.on = function on(type, listener) { 30158 if (!this.listeners[type]) { 30159 this.listeners[type] = []; 30160 } 30161 30162 this.listeners[type].push(listener); 30163 } 30164 /** 30165 * Remove a listener for a specified event type. 30166 * 30167 * @param {string} type the event name 30168 * @param {Function} listener a function previously registered for this 30169 * type of event through `on` 30170 * @return {boolean} if we could turn it off or not 30171 */ 30172 ; 30173 30174 _proto.off = function off(type, listener) { 30175 if (!this.listeners[type]) { 30176 return false; 30177 } 30178 30179 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 30180 // In Video.js we slice listener functions 30181 // on trigger so that it does not mess up the order 30182 // while we loop through. 30183 // 30184 // Here we slice on off so that the loop in trigger 30185 // can continue using it's old reference to loop without 30186 // messing up the order. 30187 30188 this.listeners[type] = this.listeners[type].slice(0); 30189 this.listeners[type].splice(index, 1); 30190 return index > -1; 30191 } 30192 /** 30193 * Trigger an event of the specified type on this stream. Any additional 30194 * arguments to this function are passed as parameters to event listeners. 30195 * 30196 * @param {string} type the event name 30197 */ 30198 ; 30199 30200 _proto.trigger = function trigger(type) { 30201 var callbacks = this.listeners[type]; 30202 30203 if (!callbacks) { 30204 return; 30205 } // Slicing the arguments on every invocation of this method 30206 // can add a significant amount of overhead. Avoid the 30207 // intermediate object creation for the common case of a 30208 // single callback argument 30209 30210 30211 if (arguments.length === 2) { 30212 var length = callbacks.length; 30213 30214 for (var i = 0; i < length; ++i) { 30215 callbacks[i].call(this, arguments[1]); 30216 } 30217 } else { 30218 var args = Array.prototype.slice.call(arguments, 1); 30219 var _length = callbacks.length; 30220 30221 for (var _i = 0; _i < _length; ++_i) { 30222 callbacks[_i].apply(this, args); 30223 } 30224 } 30225 } 30226 /** 30227 * Destroys the stream and cleans up. 30228 */ 30229 ; 30230 30231 _proto.dispose = function dispose() { 30232 this.listeners = {}; 30233 } 30234 /** 30235 * Forwards all `data` events on this stream to the destination stream. The 30236 * destination stream should provide a method `push` to receive the data 30237 * events as they arrive. 30238 * 30239 * @param {Stream} destination the stream that will receive all `data` events 30240 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 30241 */ 30242 ; 30243 30244 _proto.pipe = function pipe(destination) { 30245 this.on('data', function (data) { 30246 destination.push(data); 30247 }); 30248 }; 30249 30250 return Stream; 30251 }(); 30252 /*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */ 30253 30254 /** 30255 * Returns the subarray of a Uint8Array without PKCS#7 padding. 30256 * 30257 * @param padded {Uint8Array} unencrypted bytes that have been padded 30258 * @return {Uint8Array} the unpadded bytes 30259 * @see http://tools.ietf.org/html/rfc5652 30260 */ 30261 30262 30263 function unpad(padded) { 30264 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 30265 } 30266 /*! @name aes-decrypter @version 4.0.2 @license Apache-2.0 */ 30267 30268 /** 30269 * @file aes.js 30270 * 30271 * This file contains an adaptation of the AES decryption algorithm 30272 * from the Standford Javascript Cryptography Library. That work is 30273 * covered by the following copyright and permissions notice: 30274 * 30275 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 30276 * All rights reserved. 30277 * 30278 * Redistribution and use in source and binary forms, with or without 30279 * modification, are permitted provided that the following conditions are 30280 * met: 30281 * 30282 * 1. Redistributions of source code must retain the above copyright 30283 * notice, this list of conditions and the following disclaimer. 30284 * 30285 * 2. Redistributions in binary form must reproduce the above 30286 * copyright notice, this list of conditions and the following 30287 * disclaimer in the documentation and/or other materials provided 30288 * with the distribution. 30289 * 30290 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 30291 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 30292 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30293 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 30294 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30295 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30296 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 30297 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30298 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 30299 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 30300 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30301 * 30302 * The views and conclusions contained in the software and documentation 30303 * are those of the authors and should not be interpreted as representing 30304 * official policies, either expressed or implied, of the authors. 30305 */ 30306 30307 /** 30308 * Expand the S-box tables. 30309 * 30310 * @private 30311 */ 30312 30313 30314 const precompute = function () { 30315 const tables = [[[], [], [], [], []], [[], [], [], [], []]]; 30316 const encTable = tables[0]; 30317 const decTable = tables[1]; 30318 const sbox = encTable[4]; 30319 const sboxInv = decTable[4]; 30320 let i; 30321 let x; 30322 let xInv; 30323 const d = []; 30324 const th = []; 30325 let x2; 30326 let x4; 30327 let x8; 30328 let s; 30329 let tEnc; 30330 let tDec; // Compute double and third tables 30331 30332 for (i = 0; i < 256; i++) { 30333 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 30334 } 30335 30336 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 30337 // Compute sbox 30338 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 30339 s = s >> 8 ^ s & 255 ^ 99; 30340 sbox[x] = s; 30341 sboxInv[s] = x; // Compute MixColumns 30342 30343 x8 = d[x4 = d[x2 = d[x]]]; 30344 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 30345 tEnc = d[s] * 0x101 ^ s * 0x1010100; 30346 30347 for (i = 0; i < 4; i++) { 30348 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 30349 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 30350 } 30351 } // Compactify. Considerable speedup on Firefox. 30352 30353 30354 for (i = 0; i < 5; i++) { 30355 encTable[i] = encTable[i].slice(0); 30356 decTable[i] = decTable[i].slice(0); 30357 } 30358 30359 return tables; 30360 }; 30361 30362 let aesTables = null; 30363 /** 30364 * Schedule out an AES key for both encryption and decryption. This 30365 * is a low-level class. Use a cipher mode to do bulk encryption. 30366 * 30367 * @class AES 30368 * @param key {Array} The key as an array of 4, 6 or 8 words. 30369 */ 30370 30371 class AES { 30372 constructor(key) { 30373 /** 30374 * The expanded S-box and inverse S-box tables. These will be computed 30375 * on the client so that we don't have to send them down the wire. 30376 * 30377 * There are two tables, _tables[0] is for encryption and 30378 * _tables[1] is for decryption. 30379 * 30380 * The first 4 sub-tables are the expanded S-box with MixColumns. The 30381 * last (_tables[01][4]) is the S-box itself. 30382 * 30383 * @private 30384 */ 30385 // if we have yet to precompute the S-box tables 30386 // do so now 30387 if (!aesTables) { 30388 aesTables = precompute(); 30389 } // then make a copy of that object for use 30390 30391 30392 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()]]; 30393 let i; 30394 let j; 30395 let tmp; 30396 const sbox = this._tables[0][4]; 30397 const decTable = this._tables[1]; 30398 const keyLen = key.length; 30399 let rcon = 1; 30400 30401 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 30402 throw new Error('Invalid aes key size'); 30403 } 30404 30405 const encKey = key.slice(0); 30406 const decKey = []; 30407 this._key = [encKey, decKey]; // schedule encryption keys 30408 30409 for (i = keyLen; i < 4 * keyLen + 28; i++) { 30410 tmp = encKey[i - 1]; // apply sbox 30411 30412 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 30413 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon 30414 30415 if (i % keyLen === 0) { 30416 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 30417 rcon = rcon << 1 ^ (rcon >> 7) * 283; 30418 } 30419 } 30420 30421 encKey[i] = encKey[i - keyLen] ^ tmp; 30422 } // schedule decryption keys 30423 30424 30425 for (j = 0; i; j++, i--) { 30426 tmp = encKey[j & 3 ? i : i - 4]; 30427 30428 if (i <= 4 || j < 4) { 30429 decKey[j] = tmp; 30430 } else { 30431 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 30432 } 30433 } 30434 } 30435 /** 30436 * Decrypt 16 bytes, specified as four 32-bit words. 30437 * 30438 * @param {number} encrypted0 the first word to decrypt 30439 * @param {number} encrypted1 the second word to decrypt 30440 * @param {number} encrypted2 the third word to decrypt 30441 * @param {number} encrypted3 the fourth word to decrypt 30442 * @param {Int32Array} out the array to write the decrypted words 30443 * into 30444 * @param {number} offset the offset into the output array to start 30445 * writing results 30446 * @return {Array} The plaintext. 30447 */ 30448 30449 30450 decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 30451 const key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data 30452 30453 let a = encrypted0 ^ key[0]; 30454 let b = encrypted3 ^ key[1]; 30455 let c = encrypted2 ^ key[2]; 30456 let d = encrypted1 ^ key[3]; 30457 let a2; 30458 let b2; 30459 let c2; // key.length === 2 ? 30460 30461 const nInnerRounds = key.length / 4 - 2; 30462 let i; 30463 let kIndex = 4; 30464 const table = this._tables[1]; // load up the tables 30465 30466 const table0 = table[0]; 30467 const table1 = table[1]; 30468 const table2 = table[2]; 30469 const table3 = table[3]; 30470 const sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. 30471 30472 for (i = 0; i < nInnerRounds; i++) { 30473 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 30474 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 30475 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 30476 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 30477 kIndex += 4; 30478 a = a2; 30479 b = b2; 30480 c = c2; 30481 } // Last round. 30482 30483 30484 for (i = 0; i < 4; i++) { 30485 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 30486 a2 = a; 30487 a = b; 30488 b = c; 30489 c = d; 30490 d = a2; 30491 } 30492 } 30493 30494 } 30495 /** 30496 * @file async-stream.js 30497 */ 30498 30499 /** 30500 * A wrapper around the Stream class to use setTimeout 30501 * and run stream "jobs" Asynchronously 30502 * 30503 * @class AsyncStream 30504 * @extends Stream 30505 */ 30506 30507 30508 class AsyncStream extends Stream { 30509 constructor() { 30510 super(Stream); 30511 this.jobs = []; 30512 this.delay = 1; 30513 this.timeout_ = null; 30514 } 30515 /** 30516 * process an async job 30517 * 30518 * @private 30519 */ 30520 30521 30522 processJob_() { 30523 this.jobs.shift()(); 30524 30525 if (this.jobs.length) { 30526 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 30527 } else { 30528 this.timeout_ = null; 30529 } 30530 } 30531 /** 30532 * push a job into the stream 30533 * 30534 * @param {Function} job the job to push into the stream 30535 */ 30536 30537 30538 push(job) { 30539 this.jobs.push(job); 30540 30541 if (!this.timeout_) { 30542 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 30543 } 30544 } 30545 30546 } 30547 /** 30548 * @file decrypter.js 30549 * 30550 * An asynchronous implementation of AES-128 CBC decryption with 30551 * PKCS#7 padding. 30552 */ 30553 30554 /** 30555 * Convert network-order (big-endian) bytes into their little-endian 30556 * representation. 30557 */ 30558 30559 30560 const ntoh = function (word) { 30561 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 30562 }; 30563 /** 30564 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 30565 * 30566 * @param {Uint8Array} encrypted the encrypted bytes 30567 * @param {Uint32Array} key the bytes of the decryption key 30568 * @param {Uint32Array} initVector the initialization vector (IV) to 30569 * use for the first round of CBC. 30570 * @return {Uint8Array} the decrypted bytes 30571 * 30572 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 30573 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 30574 * @see https://tools.ietf.org/html/rfc2315 30575 */ 30576 30577 30578 const decrypt = function (encrypted, key, initVector) { 30579 // word-level access to the encrypted bytes 30580 const encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 30581 const decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output 30582 30583 const decrypted = new Uint8Array(encrypted.byteLength); 30584 const decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and 30585 // decrypted data 30586 30587 let init0; 30588 let init1; 30589 let init2; 30590 let init3; 30591 let encrypted0; 30592 let encrypted1; 30593 let encrypted2; 30594 let encrypted3; // iteration variable 30595 30596 let wordIx; // pull out the words of the IV to ensure we don't modify the 30597 // passed-in reference and easier access 30598 30599 init0 = initVector[0]; 30600 init1 = initVector[1]; 30601 init2 = initVector[2]; 30602 init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) 30603 // to each decrypted block 30604 30605 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 30606 // convert big-endian (network order) words into little-endian 30607 // (javascript order) 30608 encrypted0 = ntoh(encrypted32[wordIx]); 30609 encrypted1 = ntoh(encrypted32[wordIx + 1]); 30610 encrypted2 = ntoh(encrypted32[wordIx + 2]); 30611 encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block 30612 30613 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the 30614 // plaintext 30615 30616 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 30617 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 30618 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 30619 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round 30620 30621 init0 = encrypted0; 30622 init1 = encrypted1; 30623 init2 = encrypted2; 30624 init3 = encrypted3; 30625 } 30626 30627 return decrypted; 30628 }; 30629 /** 30630 * The `Decrypter` class that manages decryption of AES 30631 * data through `AsyncStream` objects and the `decrypt` 30632 * function 30633 * 30634 * @param {Uint8Array} encrypted the encrypted bytes 30635 * @param {Uint32Array} key the bytes of the decryption key 30636 * @param {Uint32Array} initVector the initialization vector (IV) to 30637 * @param {Function} done the function to run when done 30638 * @class Decrypter 30639 */ 30640 30641 30642 class Decrypter { 30643 constructor(encrypted, key, initVector, done) { 30644 const step = Decrypter.STEP; 30645 const encrypted32 = new Int32Array(encrypted.buffer); 30646 const decrypted = new Uint8Array(encrypted.byteLength); 30647 let i = 0; 30648 this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously 30649 30650 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 30651 30652 for (i = step; i < encrypted32.length; i += step) { 30653 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 30654 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 30655 } // invoke the done() callback when everything is finished 30656 30657 30658 this.asyncStream_.push(function () { 30659 // remove pkcs#7 padding from the decrypted bytes 30660 done(null, unpad(decrypted)); 30661 }); 30662 } 30663 /** 30664 * a getter for step the maximum number of bytes to process at one time 30665 * 30666 * @return {number} the value of step 32000 30667 */ 30668 30669 30670 static get STEP() { 30671 // 4 * 8000; 30672 return 32000; 30673 } 30674 /** 30675 * @private 30676 */ 30677 30678 30679 decryptChunk_(encrypted, key, initVector, decrypted) { 30680 return function () { 30681 const bytes = decrypt(encrypted, key, initVector); 30682 decrypted.set(bytes, encrypted.byteOffset); 30683 }; 30684 } 30685 30686 } 30687 30688 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 30689 var win; 30690 30691 if (typeof window !== "undefined") { 30692 win = window; 30693 } else if (typeof commonjsGlobal !== "undefined") { 30694 win = commonjsGlobal; 30695 } else if (typeof self !== "undefined") { 30696 win = self; 30697 } else { 30698 win = {}; 30699 } 30700 30701 var window_1 = win; 30702 30703 var isArrayBufferView = function isArrayBufferView(obj) { 30704 if (ArrayBuffer.isView === 'function') { 30705 return ArrayBuffer.isView(obj); 30706 } 30707 30708 return obj && obj.buffer instanceof ArrayBuffer; 30709 }; 30710 30711 var BigInt = window_1.BigInt || Number; 30712 [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')]; 30713 30714 (function () { 30715 var a = new Uint16Array([0xFFCC]); 30716 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 30717 30718 if (b[0] === 0xFF) { 30719 return 'big'; 30720 } 30721 30722 if (b[0] === 0xCC) { 30723 return 'little'; 30724 } 30725 30726 return 'unknown'; 30727 })(); 30728 /** 30729 * Creates an object for sending to a web worker modifying properties that are TypedArrays 30730 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 30731 * 30732 * @param {Object} message 30733 * Object of properties and values to send to the web worker 30734 * @return {Object} 30735 * Modified message with TypedArray values expanded 30736 * @function createTransferableMessage 30737 */ 30738 30739 30740 const createTransferableMessage = function (message) { 30741 const transferable = {}; 30742 Object.keys(message).forEach(key => { 30743 const value = message[key]; 30744 30745 if (isArrayBufferView(value)) { 30746 transferable[key] = { 30747 bytes: value.buffer, 30748 byteOffset: value.byteOffset, 30749 byteLength: value.byteLength 30750 }; 30751 } else { 30752 transferable[key] = value; 30753 } 30754 }); 30755 return transferable; 30756 }; 30757 /* global self */ 30758 30759 /** 30760 * Our web worker interface so that things can talk to aes-decrypter 30761 * that will be running in a web worker. the scope is passed to this by 30762 * webworkify. 30763 */ 30764 30765 30766 self.onmessage = function (event) { 30767 const data = event.data; 30768 const encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength); 30769 const key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 30770 const iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 30771 /* eslint-disable no-new, handle-callback-err */ 30772 30773 new Decrypter(encrypted, key, iv, function (err, bytes) { 30774 self.postMessage(createTransferableMessage({ 30775 source: data.source, 30776 decrypted: bytes 30777 }), [bytes.buffer]); 30778 }); 30779 /* eslint-enable */ 30780 }; 30781 })); 30782 var Decrypter = factory(workerCode); 30783 /* rollup-plugin-worker-factory end for worker!/
30783home/runner/work/http-streaming
vendor: 89,495 bytes, lines 30783-33368
30783/http-streaming/src/decrypter-worker.js */ 30784 30785 /** 30786 * Convert the properties of an HLS track into an audioTrackKind. 30787 * 30788 * @private 30789 */ 30790 30791 const audioTrackKind_ = properties => { 30792 let kind = properties.default ? 'main' : 'alternative'; 30793 30794 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) { 30795 kind = 'main-desc'; 30796 } 30797 30798 return kind; 30799 }; 30800 /** 30801 * Pause provided segment loader and playlist loader if active 30802 * 30803 * @param {SegmentLoader} segmentLoader 30804 * SegmentLoader to pause 30805 * @param {Object} mediaType 30806 * Active media type 30807 * @function stopLoaders 30808 */ 30809 30810 30811 const stopLoaders = (segmentLoader, mediaType) => { 30812 segmentLoader.abort(); 30813 segmentLoader.pause(); 30814 30815 if (mediaType && mediaType.activePlaylistLoader) { 30816 mediaType.activePlaylistLoader.pause(); 30817 mediaType.activePlaylistLoader = null; 30818 } 30819 }; 30820 /** 30821 * Start loading provided segment loader and playlist loader 30822 * 30823 * @param {PlaylistLoader} playlistLoader 30824 * PlaylistLoader to start loading 30825 * @param {Object} mediaType 30826 * Active media type 30827 * @function startLoaders 30828 */ 30829 30830 const startLoaders = (playlistLoader, mediaType) => { 30831 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 30832 // playlist loader 30833 mediaType.activePlaylistLoader = playlistLoader; 30834 playlistLoader.load(); 30835 }; 30836 /** 30837 * Returns a function to be called when the media group changes. It performs a 30838 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 30839 * change of group is merely a rendition switch of the same content at another encoding, 30840 * rather than a change of content, such as switching audio from English to Spanish. 30841 * 30842 * @param {string} type 30843 * MediaGroup type 30844 * @param {Object} settings 30845 * Object containing required information for media groups 30846 * @return {Function} 30847 * Handler for a non-destructive resync of SegmentLoader when the active media 30848 * group changes. 30849 * @function onGroupChanged 30850 */ 30851 30852 const onGroupChanged = (type, settings) => () => { 30853 const { 30854 segmentLoaders: { 30855 [type]: segmentLoader, 30856 main: mainSegmentLoader 30857 }, 30858 mediaTypes: { 30859 [type]: mediaType 30860 } 30861 } = settings; 30862 const activeTrack = mediaType.activeTrack(); 30863 const activeGroup = mediaType.getActiveGroup(); 30864 const previousActiveLoader = mediaType.activePlaylistLoader; 30865 const lastGroup = mediaType.lastGroup_; // the group did not change do nothing 30866 30867 if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) { 30868 return; 30869 } 30870 30871 mediaType.lastGroup_ = activeGroup; 30872 mediaType.lastTrack_ = activeTrack; 30873 stopLoaders(segmentLoader, mediaType); 30874 30875 if (!activeGroup || activeGroup.isMainPlaylist) { 30876 // there is no group active or active group is a main playlist and won't change 30877 return; 30878 } 30879 30880 if (!activeGroup.playlistLoader) { 30881 if (previousActiveLoader) { 30882 // The previous group had a playlist loader but the new active group does not 30883 // this means we are switching from demuxed to muxed audio. In this case we want to 30884 // do a destructive reset of the main segment loader and not restart the audio 30885 // loaders. 30886 mainSegmentLoader.resetEverything(); 30887 } 30888 30889 return; 30890 } // Non-destructive resync 30891 30892 30893 segmentLoader.resyncLoader(); 30894 startLoaders(activeGroup.playlistLoader, mediaType); 30895 }; 30896 const onGroupChanging = (type, settings) => () => { 30897 const { 30898 segmentLoaders: { 30899 [type]: segmentLoader 30900 }, 30901 mediaTypes: { 30902 [type]: mediaType 30903 } 30904 } = settings; 30905 mediaType.lastGroup_ = null; 30906 segmentLoader.abort(); 30907 segmentLoader.pause(); 30908 }; 30909 /** 30910 * Returns a function to be called when the media track changes. It performs a 30911 * destructive reset of the SegmentLoader to ensure we start loading as close to 30912 * currentTime as possible. 30913 * 30914 * @param {string} type 30915 * MediaGroup type 30916 * @param {Object} settings 30917 * Object containing required information for media groups 30918 * @return {Function} 30919 * Handler for a destructive reset of SegmentLoader when the active media 30920 * track changes. 30921 * @function onTrackChanged 30922 */ 30923 30924 const onTrackChanged = (type, settings) => () => { 30925 const { 30926 mainPlaylistLoader, 30927 segmentLoaders: { 30928 [type]: segmentLoader, 30929 main: mainSegmentLoader 30930 }, 30931 mediaTypes: { 30932 [type]: mediaType 30933 } 30934 } = settings; 30935 const activeTrack = mediaType.activeTrack(); 30936 const activeGroup = mediaType.getActiveGroup(); 30937 const previousActiveLoader = mediaType.activePlaylistLoader; 30938 const lastTrack = mediaType.lastTrack_; // track did not change, do nothing 30939 30940 if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) { 30941 return; 30942 } 30943 30944 mediaType.lastGroup_ = activeGroup; 30945 mediaType.lastTrack_ = activeTrack; 30946 stopLoaders(segmentLoader, mediaType); 30947 30948 if (!activeGroup) { 30949 // there is no group active so we do not want to restart loaders 30950 return; 30951 } 30952 30953 if (activeGroup.isMainPlaylist) { 30954 // track did not change, do nothing 30955 if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) { 30956 return; 30957 } 30958 30959 const pc = settings.vhs.playlistController_; 30960 const newPlaylist = pc.selectPlaylist(); // media will not change do nothing 30961 30962 if (pc.media() === newPlaylist) { 30963 return; 30964 } 30965 30966 mediaType.logger_(`track change. Switching main audio from ${lastTrack.id} to ${activeTrack.id}`); 30967 mainPlaylistLoader.pause(); 30968 mainSegmentLoader.resetEverything(); 30969 pc.fastQualityChange_(newPlaylist); 30970 return; 30971 } 30972 30973 if (type === 'AUDIO') { 30974 if (!activeGroup.playlistLoader) { 30975 // when switching from demuxed audio/video to muxed audio/video (noted by no 30976 // playlist loader for the audio group), we want to do a destructive reset of the 30977 // main segment loader and not restart the audio loaders 30978 mainSegmentLoader.setAudio(true); // don't have to worry about disabling the audio of the audio segment loader since 30979 // it should be stopped 30980 30981 mainSegmentLoader.resetEverything(); 30982 return; 30983 } // although the segment loader is an audio segment loader, call the setAudio 30984 // function to ensure it is prepared to re-append the init segment (or handle other 30985 // config changes) 30986 30987 30988 segmentLoader.setAudio(true); 30989 mainSegmentLoader.setAudio(false); 30990 } 30991 30992 if (previousActiveLoader === activeGroup.playlistLoader) { 30993 // Nothing has actually changed. This can happen because track change events can fire 30994 // multiple times for a "single" change. One for enabling the new active track, and 30995 // one for disabling the track that was active 30996 startLoaders(activeGroup.playlistLoader, mediaType); 30997 return; 30998 } 30999 31000 if (segmentLoader.track) { 31001 // For WebVTT, set the new text track in the segmentloader 31002 segmentLoader.track(activeTrack); 31003 } // destructive reset 31004 31005 31006 segmentLoader.resetEverything(); 31007 startLoaders(activeGroup.playlistLoader, mediaType); 31008 }; 31009 const onError = { 31010 /** 31011 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 31012 * an error. 31013 * 31014 * @param {string} type 31015 * MediaGroup type 31016 * @param {Object} settings 31017 * Object containing required information for media groups 31018 * @return {Function} 31019 * Error handler. Logs warning (or error if the playlist is excluded) to 31020 * console and switches back to default audio track. 31021 * @function onError.AUDIO 31022 */ 31023 AUDIO: (type, settings) => () => { 31024 const { 31025 mediaTypes: { 31026 [type]: mediaType 31027 }, 31028 excludePlaylist 31029 } = settings; // switch back to default audio track 31030 31031 const activeTrack = mediaType.activeTrack(); 31032 const activeGroup = mediaType.activeGroup(); 31033 const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id; 31034 const defaultTrack = mediaType.tracks[id]; 31035 31036 if (activeTrack === defaultTrack) { 31037 // Default track encountered an error. All we can do now is exclude the current 31038 // rendition and hope another will switch audio groups 31039 excludePlaylist({ 31040 error: { 31041 message: 'Problem encountered loading the default audio track.' 31042 } 31043 }); 31044 return; 31045 } 31046 31047 videojs__default["default"].log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.'); 31048 31049 for (const trackId in mediaType.tracks) { 31050 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 31051 } 31052 31053 mediaType.onTrackChanged(); 31054 }, 31055 31056 /** 31057 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 31058 * an error. 31059 * 31060 * @param {string} type 31061 * MediaGroup type 31062 * @param {Object} settings 31063 * Object containing required information for media groups 31064 * @return {Function} 31065 * Error handler. Logs warning to console and disables the active subtitle track 31066 * @function onError.SUBTITLES 31067 */ 31068 SUBTITLES: (type, settings) => () => { 31069 const { 31070 mediaTypes: { 31071 [type]: mediaType 31072 } 31073 } = settings; 31074 videojs__default["default"].log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.'); 31075 const track = mediaType.activeTrack(); 31076 31077 if (track) { 31078 track.mode = 'disabled'; 31079 } 31080 31081 mediaType.onTrackChanged(); 31082 } 31083 }; 31084 const setupListeners = { 31085 /** 31086 * Setup event listeners for audio playlist loader 31087 * 31088 * @param {string} type 31089 * MediaGroup type 31090 * @param {PlaylistLoader|null} playlistLoader 31091 * PlaylistLoader to register listeners on 31092 * @param {Object} settings 31093 * Object containing required information for media groups 31094 * @function setupListeners.AUDIO 31095 */ 31096 AUDIO: (type, playlistLoader, settings) => { 31097 if (!playlistLoader) { 31098 // no playlist loader means audio will be muxed with the video 31099 return; 31100 } 31101 31102 const { 31103 tech, 31104 requestOptions, 31105 segmentLoaders: { 31106 [type]: segmentLoader 31107 } 31108 } = settings; 31109 playlistLoader.on('loadedmetadata', () => { 31110 const media = playlistLoader.media(); 31111 segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload 31112 // permits, start downloading segments 31113 31114 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 31115 segmentLoader.load(); 31116 } 31117 }); 31118 playlistLoader.on('loadedplaylist', () => { 31119 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 31120 31121 if (!tech.paused()) { 31122 segmentLoader.load(); 31123 } 31124 }); 31125 playlistLoader.on('error', onError[type](type, settings)); 31126 }, 31127 31128 /** 31129 * Setup event listeners for subtitle playlist loader 31130 * 31131 * @param {string} type 31132 * MediaGroup type 31133 * @param {PlaylistLoader|null} playlistLoader 31134 * PlaylistLoader to register listeners on 31135 * @param {Object} settings 31136 * Object containing required information for media groups 31137 * @function setupListeners.SUBTITLES 31138 */ 31139 SUBTITLES: (type, playlistLoader, settings) => { 31140 const { 31141 tech, 31142 requestOptions, 31143 segmentLoaders: { 31144 [type]: segmentLoader 31145 }, 31146 mediaTypes: { 31147 [type]: mediaType 31148 } 31149 } = settings; 31150 playlistLoader.on('loadedmetadata', () => { 31151 const media = playlistLoader.media(); 31152 segmentLoader.playlist(media, requestOptions); 31153 segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload 31154 // permits, start downloading segments 31155 31156 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 31157 segmentLoader.load(); 31158 } 31159 }); 31160 playlistLoader.on('loadedplaylist', () => { 31161 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 31162 31163 if (!tech.paused()) { 31164 segmentLoader.load(); 31165 } 31166 }); 31167 playlistLoader.on('error', onError[type](type, settings)); 31168 } 31169 }; 31170 const initialize = { 31171 /** 31172 * Setup PlaylistLoaders and AudioTracks for the audio groups 31173 * 31174 * @param {string} type 31175 * MediaGroup type 31176 * @param {Object} settings 31177 * Object containing required information for media groups 31178 * @function initialize.AUDIO 31179 */ 31180 'AUDIO': (type, settings) => { 31181 const { 31182 vhs, 31183 sourceType, 31184 segmentLoaders: { 31185 [type]: segmentLoader 31186 }, 31187 requestOptions, 31188 main: { 31189 mediaGroups 31190 }, 31191 mediaTypes: { 31192 [type]: { 31193 groups, 31194 tracks, 31195 logger_ 31196 } 31197 }, 31198 mainPlaylistLoader 31199 } = settings; 31200 const audioOnlyMain = isAudioOnly(mainPlaylistLoader.main); // force a default if we have none 31201 31202 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 31203 mediaGroups[type] = { 31204 main: { 31205 default: { 31206 default: true 31207 } 31208 } 31209 }; 31210 31211 if (audioOnlyMain) { 31212 mediaGroups[type].main.default.playlists = mainPlaylistLoader.main.playlists; 31213 } 31214 } 31215 31216 for (const groupId in mediaGroups[type]) { 31217 if (!groups[groupId]) { 31218 groups[groupId] = []; 31219 } 31220 31221 for (const variantLabel in mediaGroups[type][groupId]) { 31222 let properties = mediaGroups[type][groupId][variantLabel]; 31223 let playlistLoader; 31224 31225 if (audioOnlyMain) { 31226 logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a main playlist`); 31227 properties.isMainPlaylist = true; 31228 playlistLoader = null; // if vhs-json was provided as the source, and the media playlist was resolved, 31229 // use the resolved media playlist object 31230 } else if (sourceType === 'vhs-json' && properties.playlists) { 31231 playlistLoader = new PlaylistLoader(properties.playlists[0], vhs, requestOptions); 31232 } else if (properties.resolvedUri) { 31233 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); // TODO: dash isn't the only type with properties.playlists 31234 // should we even have properties.playlists in this check. 31235 } else if (properties.playlists && sourceType === 'dash') { 31236 playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader); 31237 } else { 31238 // no resolvedUri means the audio is muxed with the video when using this 31239 // audio track 31240 playlistLoader = null; 31241 } 31242 31243 properties = merge$1({ 31244 id: variantLabel, 31245 playlistLoader 31246 }, properties); 31247 setupListeners[type](type, properties.playlistLoader, settings); 31248 groups[groupId].push(properties); 31249 31250 if (typeof tracks[variantLabel] === 'undefined') { 31251 const track = new videojs__default["default"].AudioTrack({ 31252 id: variantLabel, 31253 kind: audioTrackKind_(properties), 31254 enabled: false, 31255 language: properties.language, 31256 default: properties.default, 31257 label: variantLabel 31258 }); 31259 tracks[variantLabel] = track; 31260 } 31261 } 31262 } // setup single error event handler for the segment loader 31263 31264 31265 segmentLoader.on('error', onError[type](type, settings)); 31266 }, 31267 31268 /** 31269 * Setup PlaylistLoaders and TextTracks for the subtitle groups 31270 * 31271 * @param {string} type 31272 * MediaGroup type 31273 * @param {Object} settings 31274 * Object containing required information for media groups 31275 * @function initialize.SUBTITLES 31276 */ 31277 'SUBTITLES': (type, settings) => { 31278 const { 31279 tech, 31280 vhs, 31281 sourceType, 31282 segmentLoaders: { 31283 [type]: segmentLoader 31284 }, 31285 requestOptions, 31286 main: { 31287 mediaGroups 31288 }, 31289 mediaTypes: { 31290 [type]: { 31291 groups, 31292 tracks 31293 } 31294 }, 31295 mainPlaylistLoader 31296 } = settings; 31297 31298 for (const groupId in mediaGroups[type]) { 31299 if (!groups[groupId]) { 31300 groups[groupId] = []; 31301 } 31302 31303 for (const variantLabel in mediaGroups[type][groupId]) { 31304 if (!vhs.options_.useForcedSubtitles && mediaGroups[type][groupId][variantLabel].forced) { 31305 // Subtitle playlists with the forced attribute are not selectable in Safari. 31306 // According to Apple's HLS Authoring Specification: 31307 // If content has forced subtitles and regular subtitles in a given language, 31308 // the regular subtitles track in that language MUST contain both the forced 31309 // subtitles and the regular subtitles for that language. 31310 // Because of this requirement and that Safari does not add forced subtitles, 31311 // forced subtitles are skipped here to maintain consistent experience across 31312 // all platforms 31313 continue; 31314 } 31315 31316 let properties = mediaGroups[type][groupId][variantLabel]; 31317 let playlistLoader; 31318 31319 if (sourceType === 'hls') { 31320 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); 31321 } else if (sourceType === 'dash') { 31322 const playlists = properties.playlists.filter(p => p.excludeUntil !== Infinity); 31323 31324 if (!playlists.length) { 31325 return; 31326 } 31327 31328 playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader); 31329 } else if (sourceType === 'vhs-json') { 31330 playlistLoader = new PlaylistLoader( // if the vhs-json object included the media playlist, use the media playlist 31331 // as provided, otherwise use the resolved URI to load the playlist 31332 properties.playlists ? properties.playlists[0] : properties.resolvedUri, vhs, requestOptions); 31333 } 31334 31335 properties = merge$1({ 31336 id: variantLabel, 31337 playlistLoader 31338 }, properties); 31339 setupListeners[type](type, properties.playlistLoader, settings); 31340 groups[groupId].push(properties); 31341 31342 if (typeof tracks[variantLabel] === 'undefined') { 31343 const track = tech.addRemoteTextTrack({ 31344 id: variantLabel, 31345 kind: 'subtitles', 31346 default: properties.default && properties.autoselect, 31347 language: properties.language, 31348 label: variantLabel 31349 }, false).track; 31350 tracks[variantLabel] = track; 31351 } 31352 } 31353 } // setup single error event handler for the segment loader 31354 31355 31356 segmentLoader.on('error', onError[type](type, settings)); 31357 }, 31358 31359 /** 31360 * Setup TextTracks for the closed-caption groups 31361 * 31362 * @param {String} type 31363 * MediaGroup type 31364 * @param {Object} settings 31365 * Object containing required information for media groups 31366 * @function initialize['CLOSED-CAPTIONS'] 31367 */ 31368 'CLOSED-CAPTIONS': (type, settings) => { 31369 const { 31370 tech, 31371 main: { 31372 mediaGroups 31373 }, 31374 mediaTypes: { 31375 [type]: { 31376 groups, 31377 tracks 31378 } 31379 } 31380 } = settings; 31381 31382 for (const groupId in mediaGroups[type]) { 31383 if (!groups[groupId]) { 31384 groups[groupId] = []; 31385 } 31386 31387 for (const variantLabel in mediaGroups[type][groupId]) { 31388 const properties = mediaGroups[type][groupId][variantLabel]; // Look for either 608 (CCn) or 708 (SERVICEn) caption services 31389 31390 if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) { 31391 continue; 31392 } 31393 31394 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {}; 31395 let newProps = { 31396 label: variantLabel, 31397 language: properties.language, 31398 instreamId: properties.instreamId, 31399 default: properties.default && properties.autoselect 31400 }; 31401 31402 if (captionServices[newProps.instreamId]) { 31403 newProps = merge$1(newProps, captionServices[newProps.instreamId]); 31404 } 31405 31406 if (newProps.default === undefined) { 31407 delete newProps.default; 31408 } // No PlaylistLoader is required for Closed-Captions because the captions are 31409 // embedded within the video stream 31410 31411 31412 groups[groupId].push(merge$1({ 31413 id: variantLabel 31414 }, properties)); 31415 31416 if (typeof tracks[variantLabel] === 'undefined') { 31417 const track = tech.addRemoteTextTrack({ 31418 id: newProps.instreamId, 31419 kind: 'captions', 31420 default: newProps.default, 31421 language: newProps.language, 31422 label: newProps.label 31423 }, false).track; 31424 tracks[variantLabel] = track; 31425 } 31426 } 31427 } 31428 } 31429 }; 31430 31431 const groupMatch = (list, media) => { 31432 for (let i = 0; i < list.length; i++) { 31433 if (playlistMatch(media, list[i])) { 31434 return true; 31435 } 31436 31437 if (list[i].playlists && groupMatch(list[i].playlists, media)) { 31438 return true; 31439 } 31440 } 31441 31442 return false; 31443 }; 31444 /** 31445 * Returns a function used to get the active group of the provided type 31446 * 31447 * @param {string} type 31448 * MediaGroup type 31449 * @param {Object} settings 31450 * Object containing required information for media groups 31451 * @return {Function} 31452 * Function that returns the active media group for the provided type. Takes an 31453 * optional parameter {TextTrack} track. If no track is provided, a list of all 31454 * variants in the group, otherwise the variant corresponding to the provided 31455 * track is returned. 31456 * @function activeGroup 31457 */ 31458 31459 31460 const activeGroup = (type, settings) => track => { 31461 const { 31462 mainPlaylistLoader, 31463 mediaTypes: { 31464 [type]: { 31465 groups 31466 } 31467 } 31468 } = settings; 31469 const media = mainPlaylistLoader.media(); 31470 31471 if (!media) { 31472 return null; 31473 } 31474 31475 let variants = null; // set to variants to main media active group 31476 31477 if (media.attributes[type]) { 31478 variants = groups[media.attributes[type]]; 31479 } 31480 31481 const groupKeys = Object.keys(groups); 31482 31483 if (!variants) { 31484 // find the mainPlaylistLoader media 31485 // that is in a media group if we are dealing 31486 // with audio only 31487 if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.main)) { 31488 for (let i = 0; i < groupKeys.length; i++) { 31489 const groupPropertyList = groups[groupKeys[i]]; 31490 31491 if (groupMatch(groupPropertyList, media)) { 31492 variants = groupPropertyList; 31493 break; 31494 } 31495 } // use the main group if it exists 31496 31497 } else if (groups.main) { 31498 variants = groups.main; // only one group, use that one 31499 } else if (groupKeys.length === 1) { 31500 variants = groups[groupKeys[0]]; 31501 } 31502 } 31503 31504 if (typeof track === 'undefined') { 31505 return variants; 31506 } 31507 31508 if (track === null || !variants) { 31509 // An active track was specified so a corresponding group is expected. track === null 31510 // means no track is currently active so there is no corresponding group 31511 return null; 31512 } 31513 31514 return variants.filter(props => props.id === track.id)[0] || null; 31515 }; 31516 const activeTrack = { 31517 /** 31518 * Returns a function used to get the active track of type provided 31519 * 31520 * @param {string} type 31521 * MediaGroup type 31522 * @param {Object} settings 31523 * Object containing required information for media groups 31524 * @return {Function} 31525 * Function that returns the active media track for the provided type. Returns 31526 * null if no track is active 31527 * @function activeTrack.AUDIO 31528 */ 31529 AUDIO: (type, settings) => () => { 31530 const { 31531 mediaTypes: { 31532 [type]: { 31533 tracks 31534 } 31535 } 31536 } = settings; 31537 31538 for (const id in tracks) { 31539 if (tracks[id].enabled) { 31540 return tracks[id]; 31541 } 31542 } 31543 31544 return null; 31545 }, 31546 31547 /** 31548 * Returns a function used to get the active track of type provided 31549 * 31550 * @param {string} type 31551 * MediaGroup type 31552 * @param {Object} settings 31553 * Object containing required information for media groups 31554 * @return {Function} 31555 * Function that returns the active media track for the provided type. Returns 31556 * null if no track is active 31557 * @function activeTrack.SUBTITLES 31558 */ 31559 SUBTITLES: (type, settings) => () => { 31560 const { 31561 mediaTypes: { 31562 [type]: { 31563 tracks 31564 } 31565 } 31566 } = settings; 31567 31568 for (const id in tracks) { 31569 if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') { 31570 return tracks[id]; 31571 } 31572 } 31573 31574 return null; 31575 } 31576 }; 31577 const getActiveGroup = (type, { 31578 mediaTypes 31579 }) => () => { 31580 const activeTrack_ = mediaTypes[type].activeTrack(); 31581 31582 if (!activeTrack_) { 31583 return null; 31584 } 31585 31586 return mediaTypes[type].activeGroup(activeTrack_); 31587 }; 31588 /** 31589 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 31590 * Closed-Captions) specified in the main manifest. 31591 * 31592 * @param {Object} settings 31593 * Object containing required information for setting up the media groups 31594 * @param {Tech} settings.tech 31595 * The tech of the player 31596 * @param {Object} settings.requestOptions 31597 * XHR request options used by the segment loaders 31598 * @param {PlaylistLoader} settings.mainPlaylistLoader 31599 * PlaylistLoader for the main source 31600 * @param {VhsHandler} settings.vhs 31601 * VHS SourceHandler 31602 * @param {Object} settings.main 31603 * The parsed main manifest 31604 * @param {Object} settings.mediaTypes 31605 * Object to store the loaders, tracks, and utility methods for each media type 31606 * @param {Function} settings.excludePlaylist 31607 * Excludes the current rendition and forces a rendition switch. 31608 * @function setupMediaGroups 31609 */ 31610 31611 const setupMediaGroups = settings => { 31612 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 31613 initialize[type](type, settings); 31614 }); 31615 const { 31616 mediaTypes, 31617 mainPlaylistLoader, 31618 tech, 31619 vhs, 31620 segmentLoaders: { 31621 ['AUDIO']: audioSegmentLoader, 31622 main: mainSegmentLoader 31623 } 31624 } = settings; // setup active group and track getters and change event handlers 31625 31626 ['AUDIO', 'SUBTITLES'].forEach(type => { 31627 mediaTypes[type].activeGroup = activeGroup(type, settings); 31628 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 31629 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 31630 mediaTypes[type].onGroupChanging = onGroupChanging(type, settings); 31631 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 31632 mediaTypes[type].getActiveGroup = getActiveGroup(type, settings); 31633 }); // DO NOT enable the default subtitle or caption track. 31634 // DO enable the default audio track 31635 31636 const audioGroup = mediaTypes.AUDIO.activeGroup(); 31637 31638 if (audioGroup) { 31639 const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id; 31640 mediaTypes.AUDIO.tracks[groupId].enabled = true; 31641 mediaTypes.AUDIO.onGroupChanged(); 31642 mediaTypes.AUDIO.onTrackChanged(); 31643 const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup(); // a similar check for handling setAudio on each loader is run again each time the 31644 // track is changed, but needs to be handled here since the track may not be considered 31645 // changed on the first call to onTrackChanged 31646 31647 if (!activeAudioGroup.playlistLoader) { 31648 // either audio is muxed with video or the stream is audio only 31649 mainSegmentLoader.setAudio(true); 31650 } else { 31651 // audio is demuxed 31652 mainSegmentLoader.setAudio(false); 31653 audioSegmentLoader.setAudio(true); 31654 } 31655 } 31656 31657 mainPlaylistLoader.on('mediachange', () => { 31658 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged()); 31659 }); 31660 mainPlaylistLoader.on('mediachanging', () => { 31661 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanging()); 31662 }); // custom audio track change event handler for usage event 31663 31664 const onAudioTrackChanged = () => { 31665 mediaTypes.AUDIO.onTrackChanged(); 31666 tech.trigger({ 31667 type: 'usage', 31668 name: 'vhs-audio-change' 31669 }); 31670 }; 31671 31672 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 31673 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 31674 vhs.on('dispose', () => { 31675 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 31676 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 31677 }); // clear existing audio tracks and add the ones we just created 31678 31679 tech.clearTracks('audio'); 31680 31681 for (const id in mediaTypes.AUDIO.tracks) { 31682 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 31683 } 31684 }; 31685 /** 31686 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 31687 * media type 31688 * 31689 * @return {Object} 31690 * Object to store the loaders, tracks, and utility methods for each media type 31691 * @function createMediaTypes 31692 */ 31693 31694 const createMediaTypes = () => { 31695 const mediaTypes = {}; 31696 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 31697 mediaTypes[type] = { 31698 groups: {}, 31699 tracks: {}, 31700 activePlaylistLoader: null, 31701 activeGroup: noop, 31702 activeTrack: noop, 31703 getActiveGroup: noop, 31704 onGroupChanged: noop, 31705 onTrackChanged: noop, 31706 lastTrack_: null, 31707 logger_: logger(`MediaGroups[${type}]`) 31708 }; 31709 }); 31710 return mediaTypes; 31711 }; 31712 31713 /** 31714 * A utility class for setting properties and maintaining the state of the content steering manifest. 31715 * 31716 * Content Steering manifest format: 31717 * VERSION: number (required) currently only version 1 is supported. 31718 * TTL: number in seconds (optional) until the next content steering manifest reload. 31719 * RELOAD-URI: string (optional) uri to fetch the next content steering manifest. 31720 * SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values. 31721 * PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists. 31722 */ 31723 31724 class SteeringManifest { 31725 constructor() { 31726 this.priority_ = []; 31727 this.pathwayClones_ = new Map(); 31728 } 31729 31730 set version(number) { 31731 // Only version 1 is currently supported for both DASH and HLS. 31732 if (number === 1) { 31733 this.version_ = number; 31734 } 31735 } 31736 31737 set ttl(seconds) { 31738 // TTL = time-to-live, default = 300 seconds. 31739 this.ttl_ = seconds || 300; 31740 } 31741 31742 set reloadUri(uri) { 31743 if (uri) { 31744 // reload URI can be relative to the previous reloadUri. 31745 this.reloadUri_ = resolveUrl(this.reloadUri_, uri); 31746 } 31747 } 31748 31749 set priority(array) { 31750 // priority must be non-empty and unique values. 31751 if (array && array.length) { 31752 this.priority_ = array; 31753 } 31754 } 31755 31756 set pathwayClones(array) { 31757 // pathwayClones must be non-empty. 31758 if (array && array.length) { 31759 this.pathwayClones_ = new Map(array.map(clone => [clone.ID, clone])); 31760 } 31761 } 31762 31763 get version() { 31764 return this.version_; 31765 } 31766 31767 get ttl() { 31768 return this.ttl_; 31769 } 31770 31771 get reloadUri() { 31772 return this.reloadUri_; 31773 } 31774 31775 get priority() { 31776 return this.priority_; 31777 } 31778 31779 get pathwayClones() { 31780 return this.pathwayClones_; 31781 } 31782 31783 } 31784 /** 31785 * This class represents a content steering manifest and associated state. See both HLS and DASH specifications. 31786 * HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and 31787 * https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6. 31788 * DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 31789 * 31790 * @param {function} xhr for making a network request from the browser. 31791 * @param {function} bandwidth for fetching the current bandwidth from the main segment loader. 31792 */ 31793 31794 31795 class ContentSteeringController extends videojs__default["default"].EventTarget { 31796 constructor(xhr, bandwidth) { 31797 super(); 31798 this.currentPathway = null; 31799 this.defaultPathway = null; 31800 this.queryBeforeStart = false; 31801 this.availablePathways_ = new Set(); 31802 this.steeringManifest = new SteeringManifest(); 31803 this.proxyServerUrl_ = null; 31804 this.manifestType_ = null; 31805 this.ttlTimeout_ = null; 31806 this.request_ = null; 31807 this.currentPathwayClones = new Map(); 31808 this.nextPathwayClones = new Map(); 31809 this.excludedSteeringManifestURLs = new Set(); 31810 this.logger_ = logger('Content Steering'); 31811 this.xhr_ = xhr; 31812 this.getBandwidth_ = bandwidth; 31813 } 31814 /** 31815 * Assigns the content steering tag properties to the steering controller 31816 * 31817 * @param {string} baseUrl the baseURL from the main manifest for resolving the steering manifest url 31818 * @param {Object} steeringTag the content steering tag from the main manifest 31819 */ 31820 31821 31822 assignTagProperties(baseUrl, steeringTag) { 31823 this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH'; // serverUri is HLS serverURL is DASH 31824 31825 const steeringUri = steeringTag.serverUri || steeringTag.serverURL; 31826 31827 if (!steeringUri) { 31828 this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`); 31829 this.trigger('error'); 31830 return; 31831 } // Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case. 31832 31833 31834 if (steeringUri.startsWith('data:')) { 31835 this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1)); 31836 return; 31837 } // reloadUri is the resolution of the main manifest URL and steering URL. 31838 31839 31840 this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri); // pathwayId is HLS defaultServiceLocation is DASH 31841 31842 this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation; // currently only DASH supports the following properties on <ContentSteering> tags. 31843 31844 this.queryBeforeStart = steeringTag.queryBeforeStart; 31845 this.proxyServerUrl_ = steeringTag.proxyServerURL; // trigger a steering event if we have a pathway from the content steering tag. 31846 // this tells VHS which segment pathway to start with. 31847 // If queryBeforeStart is true we need to wait for the steering manifest response. 31848 31849 if (this.defaultPathway && !this.queryBeforeStart) { 31850 this.trigger('content-steering'); 31851 } 31852 } 31853 /** 31854 * Requests the content steering manifest and parse the response. This should only be called after 31855 * assignTagProperties was called with a content steering tag. 31856 * 31857 * @param {string} initialUri The optional uri to make the request with. 31858 * If set, the request should be made with exactly what is passed in this variable. 31859 * This scenario should only happen once on initalization. 31860 */ 31861 31862 31863 requestSteeringManifest(initial) { 31864 const reloadUri = this.steeringManifest.reloadUri; 31865 31866 if (!reloadUri) { 31867 return; 31868 } // We currently don't support passing MPD query parameters directly to the content steering URL as this requires 31869 // ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1. 31870 // This request URI accounts for manifest URIs that have been excluded. 31871 31872 31873 const uri = initial ? reloadUri : this.getRequestURI(reloadUri); // If there are no valid manifest URIs, we should stop content steering. 31874 31875 if (!uri) { 31876 this.logger_('No valid content steering manifest URIs. Stopping content steering.'); 31877 this.trigger('error'); 31878 this.dispose(); 31879 return; 31880 } 31881 31882 const metadata = { 31883 contentSteeringInfo: { 31884 uri 31885 } 31886 }; 31887 this.trigger({ 31888 type: 'contentsteeringloadstart', 31889 metadata 31890 }); 31891 this.request_ = this.xhr_({ 31892 uri, 31893 requestType: 'content-steering-manifest' 31894 }, (error, errorInfo) => { 31895 if (error) { 31896 // If the client receives HTTP 410 Gone in response to a manifest request, 31897 // it MUST NOT issue another request for that URI for the remainder of the 31898 // playback session. It MAY continue to use the most-recently obtained set 31899 // of Pathways. 31900 if (errorInfo.status === 410) { 31901 this.logger_(`manifest request 410 ${error}.`); 31902 this.logger_(`There will be no more content steering requests to ${uri} this session.`); 31903 this.excludedSteeringManifestURLs.add(uri); 31904 return; 31905 } // If the client receives HTTP 429 Too Many Requests with a Retry-After 31906 // header in response to a manifest request, it SHOULD wait until the time 31907 // specified by the Retry-After header to reissue the request. 31908 31909 31910 if (errorInfo.status === 429) { 31911 const retrySeconds = errorInfo.responseHeaders['retry-after']; 31912 this.logger_(`manifest request 429 ${error}.`); 31913 this.logger_(`content steering will retry in ${retrySeconds} seconds.`); 31914 this.startTTLTimeout_(parseInt(retrySeconds, 10)); 31915 return; 31916 } // If the Steering Manifest cannot be loaded and parsed correctly, the 31917 // client SHOULD continue to use the previous values and attempt to reload 31918 // it after waiting for the previously-specified TTL (or 5 minutes if 31919 // none). 31920 31921 31922 this.logger_(`manifest failed to load ${error}.`); 31923 this.startTTLTimeout_(); 31924 return; 31925 } 31926 31927 this.trigger({ 31928 type: 'contentsteeringloadcomplete', 31929 metadata 31930 }); 31931 let steeringManifestJson; 31932 31933 try { 31934 steeringManifestJson = JSON.parse(this.request_.responseText); 31935 } catch (parseError) { 31936 const errorMetadata = { 31937 errorType: videojs__default["default"].Error.StreamingContentSteeringParserError, 31938 error: parseError 31939 }; 31940 this.trigger({ 31941 type: 'error', 31942 metadata: errorMetadata 31943 }); 31944 } 31945 31946 this.assignSteeringProperties_(steeringManifestJson); 31947 const parsedMetadata = { 31948 contentSteeringInfo: metadata.contentSteeringInfo, 31949 contentSteeringManifest: { 31950 version: this.steeringManifest.version, 31951 reloadUri: this.steeringManifest.reloadUri, 31952 priority: this.steeringManifest.priority 31953 } 31954 }; 31955 this.trigger({ 31956 type: 'contentsteeringparsed', 31957 metadata: parsedMetadata 31958 }); 31959 this.startTTLTimeout_(); 31960 }); 31961 } 31962 /** 31963 * Set the proxy server URL and add the steering manifest url as a URI encoded parameter. 31964 * 31965 * @param {string} steeringUrl the steering manifest url 31966 * @return the steering manifest url to a proxy server with all parameters set 31967 */ 31968 31969 31970 setProxyServerUrl_(steeringUrl) { 31971 const steeringUrlObject = new window.URL(steeringUrl); 31972 const proxyServerUrlObject = new window.URL(this.proxyServerUrl_); 31973 proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString())); 31974 return this.setSteeringParams_(proxyServerUrlObject.toString()); 31975 } 31976 /** 31977 * Decodes and parses the data uri encoded steering manifest 31978 * 31979 * @param {string} dataUri the data uri to be decoded and parsed. 31980 */ 31981 31982 31983 decodeDataUriManifest_(dataUri) { 31984 const steeringManifestJson = JSON.parse(window.atob(dataUri)); 31985 this.assignSteeringProperties_(steeringManifestJson); 31986 } 31987 /** 31988 * Set the HLS or DASH content steering manifest request query parameters. For example: 31989 * _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT> 31990 * _DASH_pathway and _DASH_throughput 31991 * 31992 * @param {string} uri to add content steering server parameters to. 31993 * @return a new uri as a string with the added steering query parameters. 31994 */ 31995 31996 31997 setSteeringParams_(url) { 31998 const urlObject = new window.URL(url); 31999 const path = this.getPathway(); 32000 const networkThroughput = this.getBandwidth_(); 32001 32002 if (path) { 32003 const pathwayKey = `_${this.manifestType_}_pathway`; 32004 urlObject.searchParams.set(pathwayKey, path); 32005 } 32006 32007 if (networkThroughput) { 32008 const throughputKey = `_${this.manifestType_}_throughput`; 32009 urlObject.searchParams.set(throughputKey, networkThroughput); 32010 } 32011 32012 return urlObject.toString(); 32013 } 32014 /** 32015 * Assigns the current steering manifest properties and to the SteeringManifest object 32016 * 32017 * @param {Object} steeringJson the raw JSON steering manifest 32018 */ 32019 32020 32021 assignSteeringProperties_(steeringJson) { 32022 this.steeringManifest.version = steeringJson.VERSION; 32023 32024 if (!this.steeringManifest.version) { 32025 this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`); 32026 this.trigger('error'); 32027 return; 32028 } 32029 32030 this.steeringManifest.ttl = steeringJson.TTL; 32031 this.steeringManifest.reloadUri = steeringJson['RELOAD-URI']; // HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional 32032 32033 this.steeringManifest.priority = steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY']; // Pathway clones to be created/updated in HLS. 32034 // See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 32035 32036 this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES']; 32037 this.nextPathwayClones = this.steeringManifest.pathwayClones; // 1. apply first pathway from the array. 32038 // 2. if first pathway doesn't exist in manifest, try next pathway. 32039 // a. if all pathways are exhausted, ignore the steering manifest priority. 32040 // 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway. 32041 // a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response, 32042 // the excluded pathway will be ignored. 32043 // See excludePathway usage in excludePlaylist(). 32044 // If there are no available pathways, we need to stop content steering. 32045 32046 if (!this.availablePathways_.size) { 32047 this.logger_('There are no available pathways for content steering. Ending content steering.'); 32048 this.trigger('error'); 32049 this.dispose(); 32050 } 32051 32052 const chooseNextPathway = pathwaysByPriority => { 32053 for (const path of pathwaysByPriority) { 32054 if (this.availablePathways_.has(path)) { 32055 return path; 32056 } 32057 } // If no pathway matches, ignore the manifest and choose the first available. 32058 32059 32060 return [...this.availablePathways_][0]; 32061 }; 32062 32063 const nextPathway = chooseNextPathway(this.steeringManifest.priority); 32064 32065 if (this.currentPathway !== nextPathway) { 32066 this.currentPathway = nextPathway; 32067 this.trigger('content-steering'); 32068 } 32069 } 32070 /** 32071 * Returns the pathway to use for steering decisions 32072 * 32073 * @return {string} returns the current pathway or the default 32074 */ 32075 32076 32077 getPathway() { 32078 return this.currentPathway || this.defaultPathway; 32079 } 32080 /** 32081 * Chooses the manifest request URI based on proxy URIs and server URLs. 32082 * Also accounts for exclusion on certain manifest URIs. 32083 * 32084 * @param {string} reloadUri the base uri before parameters 32085 * 32086 * @return {string} the final URI for the request to the manifest server. 32087 */ 32088 32089 32090 getRequestURI(reloadUri) { 32091 if (!reloadUri) { 32092 return null; 32093 } 32094 32095 const isExcluded = uri => this.excludedSteeringManifestURLs.has(uri); 32096 32097 if (this.proxyServerUrl_) { 32098 const proxyURI = this.setProxyServerUrl_(reloadUri); 32099 32100 if (!isExcluded(proxyURI)) { 32101 return proxyURI; 32102 } 32103 } 32104 32105 const steeringURI = this.setSteeringParams_(reloadUri); 32106 32107 if (!isExcluded(steeringURI)) { 32108 return steeringURI; 32109 } // Return nothing if all valid manifest URIs are excluded. 32110 32111 32112 return null; 32113 } 32114 /** 32115 * Start the timeout for re-requesting the steering manifest at the TTL interval. 32116 * 32117 * @param {number} ttl time in seconds of the timeout. Defaults to the 32118 * ttl interval in the steering manifest 32119 */ 32120 32121 32122 startTTLTimeout_(ttl = this.steeringManifest.ttl) { 32123 // 300 (5 minutes) is the default value. 32124 const ttlMS = ttl * 1000; 32125 this.ttlTimeout_ = window.setTimeout(() => { 32126 this.requestSteeringManifest(); 32127 }, ttlMS); 32128 } 32129 /** 32130 * Clear the TTL timeout if necessary. 32131 */ 32132 32133 32134 clearTTLTimeout_() { 32135 window.clearTimeout(this.ttlTimeout_); 32136 this.ttlTimeout_ = null; 32137 } 32138 /** 32139 * aborts any current steering xhr and sets the current request object to null 32140 */ 32141 32142 32143 abort() { 32144 if (this.request_) { 32145 this.request_.abort(); 32146 } 32147 32148 this.request_ = null; 32149 } 32150 /** 32151 * aborts steering requests clears the ttl timeout and resets all properties. 32152 */ 32153 32154 32155 dispose() { 32156 this.off('content-steering'); 32157 this.off('error'); 32158 this.abort(); 32159 this.clearTTLTimeout_(); 32160 this.currentPathway = null; 32161 this.defaultPathway = null; 32162 this.queryBeforeStart = null; 32163 this.proxyServerUrl_ = null; 32164 this.manifestType_ = null; 32165 this.ttlTimeout_ = null; 32166 this.request_ = null; 32167 this.excludedSteeringManifestURLs = new Set(); 32168 this.availablePathways_ = new Set(); 32169 this.steeringManifest = new SteeringManifest(); 32170 } 32171 /** 32172 * adds a pathway to the available pathways set 32173 * 32174 * @param {string} pathway the pathway string to add 32175 */ 32176 32177 32178 addAvailablePathway(pathway) { 32179 if (pathway) { 32180 this.availablePathways_.add(pathway); 32181 } 32182 } 32183 /** 32184 * Clears all pathways from the available pathways set 32185 */ 32186 32187 32188 clearAvailablePathways() { 32189 this.availablePathways_.clear(); 32190 } 32191 /** 32192 * Removes a pathway from the available pathways set. 32193 */ 32194 32195 32196 excludePathway(pathway) { 32197 return this.availablePathways_.delete(pathway); 32198 } 32199 /** 32200 * Checks the refreshed DASH manifest content steering tag for changes. 32201 * 32202 * @param {string} baseURL new steering tag on DASH manifest refresh 32203 * @param {Object} newTag the new tag to check for changes 32204 * @return a true or false whether the new tag has different values 32205 */ 32206 32207 32208 didDASHTagChange(baseURL, newTag) { 32209 return !newTag && this.steeringManifest.reloadUri || newTag && (resolveUrl(baseURL, newTag.serverURL) !== this.steeringManifest.reloadUri || newTag.defaultServiceLocation !== this.defaultPathway || newTag.queryBeforeStart !== this.queryBeforeStart || newTag.proxyServerURL !== this.proxyServerUrl_); 32210 } 32211 32212 getAvailablePathways() { 32213 return this.availablePathways_; 32214 } 32215 32216 } 32217 32218 const debounce = (callback, wait) => { 32219 let timeoutId = null; 32220 return (...args) => { 32221 clearTimeout(timeoutId); 32222 timeoutId = setTimeout(() => { 32223 callback.apply(null, args); 32224 }, wait); 32225 }; 32226 }; 32227 32228 const ABORT_EARLY_EXCLUSION_SECONDS = 10; 32229 let Vhs$1; // SegmentLoader stats that need to have each loader's 32230 // values summed to calculate the final value 32231 32232 const loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred', 'mediaAppends']; 32233 32234 const sumLoaderStat = function (stat) { 32235 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 32236 }; 32237 32238 const shouldSwitchToMedia = function ({ 32239 currentPlaylist, 32240 buffered, 32241 currentTime, 32242 nextPlaylist, 32243 bufferLowWaterLine, 32244 bufferHighWaterLine, 32245 duration, 32246 bufferBasedABR, 32247 log 32248 }) { 32249 // we have no other playlist to switch to 32250 if (!nextPlaylist) { 32251 videojs__default["default"].log.warn('We received no playlist to switch to. Please check your stream.'); 32252 return false; 32253 } 32254 32255 const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`; 32256 32257 if (!currentPlaylist) { 32258 log(`${sharedLogLine} as current playlist is not set`); 32259 return true; 32260 } // no need to switch if playlist is the same 32261 32262 32263 if (nextPlaylist.id === currentPlaylist.id) { 32264 return false; 32265 } // determine if current time is in a buffered range. 32266 32267 32268 const isBuffered = Boolean(findRange(buffered, currentTime).length); // If the playlist is live, then we want to not take low water line into account. 32269 // This is because in LIVE, the player plays 3 segments from the end of the 32270 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 32271 // in those segments, a viewer will never experience a rendition upswitch. 32272 32273 if (!currentPlaylist.endList) { 32274 // For LLHLS live streams, don't switch renditions before playback has started, as it almost 32275 // doubles the time to first playback. 32276 if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') { 32277 log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`); 32278 return false; 32279 } 32280 32281 log(`${sharedLogLine} as current playlist is live`); 32282 return true; 32283 } 32284 32285 const forwardBuffer = timeAheadOf(buffered, currentTime); 32286 const maxBufferLowWaterLine = bufferBasedABR ? Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE; // For the same reason as LIVE, we ignore the low water line when the VOD 32287 // duration is below the max potential low water line 32288 32289 if (duration < maxBufferLowWaterLine) { 32290 log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`); 32291 return true; 32292 } 32293 32294 const nextBandwidth = nextPlaylist.attributes.BANDWIDTH; 32295 const currBandwidth = currentPlaylist.attributes.BANDWIDTH; // when switching down, if our buffer is lower than the high water line, 32296 // we can switch down 32297 32298 if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) { 32299 let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`; 32300 32301 if (bufferBasedABR) { 32302 logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`; 32303 } 32304 32305 log(logLine); 32306 return true; 32307 } // and if our buffer is higher than the low water line, 32308 // we can switch up 32309 32310 32311 if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) { 32312 let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`; 32313 32314 if (bufferBasedABR) { 32315 logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`; 32316 } 32317 32318 log(logLine); 32319 return true; 32320 } 32321 32322 log(`not ${sharedLogLine} as no switching criteria met`); 32323 return false; 32324 }; 32325 /** 32326 * the main playlist controller controller all interactons 32327 * between playlists and segmentloaders. At this time this mainly 32328 * involves a main playlist and a series of audio playlists 32329 * if they are available 32330 * 32331 * @class PlaylistController 32332 * @extends videojs.EventTarget 32333 */ 32334 32335 32336 class PlaylistController extends videojs__default["default"].EventTarget { 32337 constructor(options) { 32338 super(); // Adding a slight debounce to avoid duplicate calls during rapid quality changes, for example: 32339 // When selecting quality from the quality list, 32340 // where we may have multiple bandwidth profiles for the same vertical resolution. 32341 32342 this.fastQualityChange_ = debounce(this.fastQualityChange_.bind(this), 100); 32343 const { 32344 src, 32345 withCredentials, 32346 tech, 32347 bandwidth, 32348 externVhs, 32349 useCueTags, 32350 playlistExclusionDuration, 32351 enableLowInitialPlaylist, 32352 sourceType, 32353 cacheEncryptionKeys, 32354 bufferBasedABR, 32355 leastPixelDiffSelector, 32356 captionServices, 32357 experimentalUseMMS 32358 } = options; 32359 32360 if (!src) { 32361 throw new Error('A non-empty playlist URL or JSON manifest string is required'); 32362 } 32363 32364 let { 32365 maxPlaylistRetries 32366 } = options; 32367 32368 if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') { 32369 maxPlaylistRetries = Infinity; 32370 } 32371 32372 Vhs$1 = externVhs; 32373 this.bufferBasedABR = Boolean(bufferBasedABR); 32374 this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector); 32375 this.withCredentials = withCredentials; 32376 this.tech_ = tech; 32377 this.vhs_ = tech.vhs; 32378 this.player_ = options.player_; 32379 this.sourceType_ = sourceType; 32380 this.useCueTags_ = useCueTags; 32381 this.playlistExclusionDuration = playlistExclusionDuration; 32382 this.maxPlaylistRetries = maxPlaylistRetries; 32383 this.enableLowInitialPlaylist = enableLowInitialPlaylist; 32384 this.usingManagedMediaSource_ = false; 32385 32386 if (this.useCueTags_) { 32387 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); 32388 this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 32389 } 32390 32391 this.requestOptions_ = { 32392 withCredentials, 32393 maxPlaylistRetries, 32394 timeout: null 32395 }; 32396 this.on('error', this.pauseLoading); 32397 this.mediaTypes_ = createMediaTypes(); 32398 32399 if (experimentalUseMMS && window.ManagedMediaSource) { 32400 // Airplay source not yet implemented. Remote playback must be disabled. 32401 this.tech_.el_.disableRemotePlayback = true; 32402 this.mediaSource = new window.ManagedMediaSource(); 32403 this.usingManagedMediaSource_ = true; 32404 videojs__default["default"].log('Using ManagedMediaSource'); 32405 } else if (window.MediaSource) { 32406 this.mediaSource = new window.MediaSource(); 32407 } 32408 32409 this.handleDurationChange_ = this.handleDurationChange_.bind(this); 32410 this.handleSourceOpen_ = this.handleSourceOpen_.bind(this); 32411 this.handleSourceEnded_ = this.handleSourceEnded_.bind(this); 32412 this.load = this.load.bind(this); 32413 this.pause = this.pause.bind(this); 32414 this.mediaSource.addEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 32415 32416 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_); 32417 this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_); 32418 this.mediaSource.addEventListener('startstreaming', this.load); 32419 this.mediaSource.addEventListener('endstreaming', this.pause); // we don't have to handle sourceclose since dispose will handle termination of 32420 // everything, and the MediaSource should not be detached without a proper disposal 32421 32422 this.seekable_ = createTimeRanges(); 32423 this.hasPlayed_ = false; 32424 this.syncController_ = new SyncController(options); 32425 this.segmentMetadataTrack_ = tech.addRemoteTextTrack({ 32426 kind: 'metadata', 32427 label: 'segment-metadata' 32428 }, false).track; 32429 this.segmentMetadataTrack_.mode = 'hidden'; 32430 this.decrypter_ = new Decrypter(); 32431 this.sourceUpdater_ = new SourceUpdater(this.mediaSource); 32432 this.inbandTextTracks_ = {}; 32433 this.timelineChangeController_ = new TimelineChangeController(); 32434 this.keyStatusMap_ = new Map(); 32435 const segmentLoaderSettings = { 32436 vhs: this.vhs_, 32437 parse708captions: options.parse708captions, 32438 useDtsForTimestampOffset: options.useDtsForTimestampOffset, 32439 captionServices, 32440 mediaSource: this.mediaSource, 32441 currentTime: this.tech_.currentTime.bind(this.tech_), 32442 seekable: () => this.seekable(), 32443 seeking: () => this.tech_.seeking(), 32444 duration: () => this.duration(), 32445 hasPlayed: () => this.hasPlayed_, 32446 goalBufferLength: () => this.goalBufferLength(), 32447 bandwidth, 32448 syncController: this.syncController_, 32449 decrypter: this.decrypter_, 32450 sourceType: this.sourceType_, 32451 inbandTextTracks: this.inbandTextTracks_, 32452 cacheEncryptionKeys, 32453 sourceUpdater: this.sourceUpdater_, 32454 timelineChangeController: this.timelineChangeController_, 32455 exactManifestTimings: options.exactManifestTimings, 32456 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) 32457 }; // The source type check not only determines whether a special DASH playlist loader 32458 // should be used, but also covers the case where the provided src is a vhs-json 32459 // manifest object (instead of a URL). In the case of vhs-json, the default 32460 // PlaylistLoader should be used. 32461 32462 this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ? new DashPlaylistLoader(src, this.vhs_, merge$1(this.requestOptions_, { 32463 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) 32464 })) : new PlaylistLoader(src, this.vhs_, merge$1(this.requestOptions_, { 32465 addDateRangesToTextTrack: this.addDateRangesToTextTrack_.bind(this) 32466 })); 32467 this.setupMainPlaylistLoaderListeners_(); // setup segment loaders 32468 // combined audio/video or just video when alternate audio track is selected 32469 32470 this.mainSegmentLoader_ = new SegmentLoader(merge$1(segmentLoaderSettings, { 32471 segmentMetadataTrack: this.segmentMetadataTrack_, 32472 loaderType: 'main' 32473 }), options); // alternate audio track 32474 32475 this.audioSegmentLoader_ = new SegmentLoader(merge$1(segmentLoaderSettings, { 32476 loaderType: 'audio' 32477 }), options); 32478 this.subtitleSegmentLoader_ = new VTTSegmentLoader(merge$1(segmentLoaderSettings, { 32479 loaderType: 'vtt', 32480 featuresNativeTextTracks: this.tech_.featuresNativeTextTracks, 32481 loadVttJs: () => new Promise((resolve, reject) => { 32482 function onLoad() { 32483 tech.off('vttjserror', onError); 32484 resolve(); 32485 } 32486 32487 function onError() { 32488 tech.off('vttjsloaded', onLoad); 32489 reject(); 32490 } 32491 32492 tech.one('vttjsloaded', onLoad); 32493 tech.one('vttjserror', onError); // safe to call multiple times, script will be loaded only once: 32494 32495 tech.addWebVttScript_(); 32496 }) 32497 }), options); 32498 32499 const getBandwidth = () => { 32500 return this.mainSegmentLoader_.bandwidth; 32501 }; 32502 32503 this.contentSteeringController_ = new ContentSteeringController(this.vhs_.xhr, getBandwidth); 32504 this.setupSegmentLoaderListeners_(); 32505 32506 if (this.bufferBasedABR) { 32507 this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_()); 32508 this.tech_.on('pause', () => this.stopABRTimer_()); 32509 this.tech_.on('play', () => this.startABRTimer_()); 32510 } // Create SegmentLoader stat-getters 32511 // mediaRequests_ 32512 // mediaRequestsAborted_ 32513 // mediaRequestsTimedout_ 32514 // mediaRequestsErrored_ 32515 // mediaTransferDuration_ 32516 // mediaBytesTransferred_ 32517 // mediaAppends_ 32518 32519 32520 loaderStats.forEach(stat => { 32521 this[stat + '_'] = sumLoaderStat.bind(this, stat); 32522 }); 32523 this.logger_ = logger('pc'); 32524 this.triggeredFmp4Usage = false; 32525 32526 if (this.tech_.preload() === 'none') { 32527 this.loadOnPlay_ = () => { 32528 this.loadOnPlay_ = null; 32529 this.mainPlaylistLoader_.load(); 32530 }; 32531 32532 this.tech_.one('play', this.loadOnPlay_); 32533 } else { 32534 this.mainPlaylistLoader_.load(); 32535 } 32536 32537 this.timeToLoadedData__ = -1; 32538 this.mainAppendsToLoadedData__ = -1; 32539 this.audioAppendsToLoadedData__ = -1; 32540 const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart'; // start the first frame timer on loadstart or play (for preload none) 32541 32542 this.tech_.one(event, () => { 32543 const timeToLoadedDataStart = Date.now(); 32544 this.tech_.one('loadeddata', () => { 32545 this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart; 32546 this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends; 32547 this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends; 32548 }); 32549 }); 32550 } 32551 32552 mainAppendsToLoadedData_() { 32553 return this.mainAppendsToLoadedData__; 32554 } 32555 32556 audioAppendsToLoadedData_() { 32557 return this.audioAppendsToLoadedData__; 32558 } 32559 32560 appendsToLoadedData_() { 32561 const main = this.mainAppendsToLoadedData_(); 32562 const audio = this.audioAppendsToLoadedData_(); 32563 32564 if (main === -1 || audio === -1) { 32565 return -1; 32566 } 32567 32568 return main + audio; 32569 } 32570 32571 timeToLoadedData_() { 32572 return this.timeToLoadedData__; 32573 } 32574 /** 32575 * Run selectPlaylist and switch to the new playlist if we should 32576 * 32577 * @param {string} [reason=abr] a reason for why the ABR check is made 32578 * @private 32579 */ 32580 32581 32582 checkABR_(reason = 'abr') { 32583 const nextPlaylist = this.selectPlaylist(); 32584 32585 if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) { 32586 this.switchMedia_(nextPlaylist, reason); 32587 } 32588 } 32589 32590 switchMedia_(playlist, cause, delay) { 32591 const oldMedia = this.media(); 32592 const oldId = oldMedia && (oldMedia.id || oldMedia.uri); 32593 const newId = playlist && (playlist.id || playlist.uri); 32594 32595 if (oldId && oldId !== newId) { 32596 this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`); 32597 const metadata = { 32598 renditionInfo: { 32599 id: newId, 32600 bandwidth: playlist.attributes.BANDWIDTH, 32601 resolution: playlist.attributes.RESOLUTION, 32602 codecs: playlist.attributes.CODECS 32603 }, 32604 cause 32605 }; 32606 this.trigger({ 32607 type: 'renditionselected', 32608 metadata 32609 }); 32610 this.tech_.trigger({ 32611 type: 'usage', 32612 name: `vhs-rendition-change-${cause}` 32613 }); 32614 } 32615 32616 this.mainPlaylistLoader_.media(playlist, delay); 32617 } 32618 /** 32619 * A function that ensures we switch our playlists inside of `mediaTypes` 32620 * to match the current `serviceLocation` provided by the contentSteering controller. 32621 * We want to check media types of `AUDIO`, `SUBTITLES`, and `CLOSED-CAPTIONS`. 32622 * 32623 * This should only be called on a DASH playback scenario while using content steering. 32624 * This is necessary due to differences in how media in HLS manifests are generally tied to 32625 * a video playlist, where in DASH that is not always the case. 32626 */ 32627 32628 32629 switchMediaForDASHContentSteering_() { 32630 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 32631 const mediaType = this.mediaTypes_[type]; 32632 const activeGroup = mediaType ? mediaType.activeGroup() : null; 32633 const pathway = this.contentSteeringController_.getPathway(); 32634 32635 if (activeGroup && pathway) { 32636 // activeGroup can be an array or a single group 32637 const mediaPlaylists = activeGroup.length ? activeGroup[0].playlists : activeGroup.playlists; 32638 const dashMediaPlaylists = mediaPlaylists.filter(p => p.attributes.serviceLocation === pathway); // Switch the current active playlist to the correct CDN 32639 32640 if (dashMediaPlaylists.length) { 32641 this.mediaTypes_[type].activePlaylistLoader.media(dashMediaPlaylists[0]); 32642 } 32643 } 32644 }); 32645 } 32646 /** 32647 * Start a timer that periodically calls checkABR_ 32648 * 32649 * @private 32650 */ 32651 32652 32653 startABRTimer_() { 32654 this.stopABRTimer_(); 32655 this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250); 32656 } 32657 /** 32658 * Stop the timer that periodically calls checkABR_ 32659 * 32660 * @private 32661 */ 32662 32663 32664 stopABRTimer_() { 32665 // if we're scrubbing, we don't need to pause. 32666 // This getter will be added to Video.js in version 7.11. 32667 if (this.tech_.scrubbing && this.tech_.scrubbing()) { 32668 return; 32669 } 32670 32671 window.clearInterval(this.abrTimer_); 32672 this.abrTimer_ = null; 32673 } 32674 /** 32675 * Get a list of playlists for the currently selected audio playlist 32676 * 32677 * @return {Array} the array of audio playlists 32678 */ 32679 32680 32681 getAudioTrackPlaylists_() { 32682 const main = this.main(); 32683 const defaultPlaylists = main && main.playlists || []; // if we don't have any audio groups then we can only 32684 // assume that the audio tracks are contained in main 32685 // playlist array, use that or an empty array. 32686 32687 if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) { 32688 return defaultPlaylists; 32689 } 32690 32691 const AUDIO = main.mediaGroups.AUDIO; 32692 const groupKeys = Object.keys(AUDIO); 32693 let track; // get the current active track 32694 32695 if (Object.keys(this.mediaTypes_.AUDIO.groups).length) { 32696 track = this.mediaTypes_.AUDIO.activeTrack(); // or get the default track from main if mediaTypes_ isn't setup yet 32697 } else { 32698 // default group is `main` or just the first group. 32699 const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]]; 32700 32701 for (const label in defaultGroup) { 32702 if (defaultGroup[label].default) { 32703 track = { 32704 label 32705 }; 32706 break; 32707 } 32708 } 32709 } // no active track no playlists. 32710 32711 32712 if (!track) { 32713 return defaultPlaylists; 32714 } 32715 32716 const playlists = []; // get all of the playlists that are possible for the 32717 // active track. 32718 32719 for (const group in AUDIO) { 32720 if (AUDIO[group][track.label]) { 32721 const properties = AUDIO[group][track.label]; 32722 32723 if (properties.playlists && properties.playlists.length) { 32724 playlists.push.apply(playlists, properties.playlists); 32725 } else if (properties.uri) { 32726 playlists.push(properties); 32727 } else if (main.playlists.length) { 32728 // if an audio group does not have a uri 32729 // see if we have main playlists that use it as a group. 32730 // if we do then add those to the playlists list. 32731 for (let i = 0; i < main.playlists.length; i++) { 32732 const playlist = main.playlists[i]; 32733 32734 if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) { 32735 playlists.push(playlist); 32736 } 32737 } 32738 } 32739 } 32740 } 32741 32742 if (!playlists.length) { 32743 return defaultPlaylists; 32744 } 32745 32746 return playlists; 32747 } 32748 /** 32749 * Register event handlers on the main playlist loader. A helper 32750 * function for construction time. 32751 * 32752 * @private 32753 */ 32754 32755 32756 setupMainPlaylistLoaderListeners_() { 32757 this.mainPlaylistLoader_.on('loadedmetadata', () => { 32758 const media = this.mainPlaylistLoader_.media(); 32759 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 32760 // timeout the request. 32761 32762 if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) { 32763 this.requestOptions_.timeout = 0; 32764 } else { 32765 this.requestOptions_.timeout = requestTimeout; 32766 } // if this isn't a live video and preload permits, start 32767 // downloading segments 32768 32769 32770 if (media.endList && this.tech_.preload() !== 'none') { 32771 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 32772 this.mainSegmentLoader_.load(); 32773 } 32774 32775 setupMediaGroups({ 32776 sourceType: this.sourceType_, 32777 segmentLoaders: { 32778 AUDIO: this.audioSegmentLoader_, 32779 SUBTITLES: this.subtitleSegmentLoader_, 32780 main: this.mainSegmentLoader_ 32781 }, 32782 tech: this.tech_, 32783 requestOptions: this.requestOptions_, 32784 mainPlaylistLoader: this.mainPlaylistLoader_, 32785 vhs: this.vhs_, 32786 main: this.main(), 32787 mediaTypes: this.mediaTypes_, 32788 excludePlaylist: this.excludePlaylist.bind(this) 32789 }); 32790 this.triggerPresenceUsage_(this.main(), media); 32791 this.setupFirstPlay(); 32792 32793 if (!this.mediaTypes_.AUDIO.activePlaylistLoader || this.mediaTypes_.AUDIO.activePlaylistLoader.media()) { 32794 this.trigger('selectedinitialmedia'); 32795 } else { 32796 // We must wait for the active audio playlist loader to 32797 // finish setting up before triggering this event so the 32798 // representations API and EME setup is correct 32799 this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => { 32800 this.trigger('selectedinitialmedia'); 32801 }); 32802 } 32803 }); 32804 this.mainPlaylistLoader_.on('loadedplaylist', () => { 32805 if (this.loadOnPlay_) { 32806 this.tech_.off('play', this.loadOnPlay_); 32807 } 32808 32809 let updatedPlaylist = this.mainPlaylistLoader_.media(); 32810 32811 if (!updatedPlaylist) { 32812 // Add content steering listeners on first load and init. 32813 this.attachContentSteeringListeners_(); 32814 this.initContentSteeringController_(); // exclude any variants that are not supported by the browser before selecting 32815 // an initial media as the playlist selectors do not consider browser support 32816 32817 this.excludeUnsupportedVariants_(); 32818 let selectedMedia; 32819 32820 if (this.enableLowInitialPlaylist) { 32821 selectedMedia = this.selectInitialPlaylist(); 32822 } 32823 32824 if (!selectedMedia) { 32825 selectedMedia = this.selectPlaylist(); 32826 } 32827 32828 if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) { 32829 return; 32830 } 32831 32832 this.initialMedia_ = selectedMedia; 32833 this.switchMedia_(this.initialMedia_, 'initial'); // Under the standard case where a source URL is provided, loadedplaylist will 32834 // fire again since the playlist will be requested. In the case of vhs-json 32835 // (where the manifest object is provided as the source), when the media 32836 // playlist's `segments` list is already available, a media playlist won't be 32837 // requested, and loadedplaylist won't fire again, so the playlist handler must be 32838 // called on its own here. 32839 32840 const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments; 32841 32842 if (!haveJsonSource) { 32843 return; 32844 } 32845 32846 updatedPlaylist = this.initialMedia_; 32847 } 32848 32849 this.handleUpdatedMediaPlaylist(updatedPlaylist); 32850 }); 32851 this.mainPlaylistLoader_.on('error', () => { 32852 const error = this.mainPlaylistLoader_.error; 32853 this.excludePlaylist({ 32854 playlistToExclude: error.playlist, 32855 error 32856 }); 32857 }); 32858 this.mainPlaylistLoader_.on('mediachanging', () => { 32859 this.mainSegmentLoader_.abort(); 32860 this.mainSegmentLoader_.pause(); 32861 }); 32862 this.mainPlaylistLoader_.on('mediachange', () => { 32863 const media = this.mainPlaylistLoader_.media(); 32864 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 32865 // timeout the request. 32866 32867 if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) { 32868 this.requestOptions_.timeout = 0; 32869 } else { 32870 this.requestOptions_.timeout = requestTimeout; 32871 } 32872 32873 if (this.sourceType_ === 'dash') { 32874 // we don't want to re-request the same hls playlist right after it was changed 32875 // Initially it was implemented as workaround to restart playlist loader for live 32876 // when playlist loader is paused because of playlist exclusions: 32877 // see: https://github.com/videojs/http-streaming/pull/1339 32878 // but this introduces duplicate "loadedplaylist" event. 32879 // Ideally we want to re-think playlist loader life-cycle events, 32880 // but simply checking "paused" state should help a lot 32881 if (this.mainPlaylistLoader_.isPaused) { 32882 this.mainPlaylistLoader_.load(); 32883 } 32884 } // TODO: Create a new event on the PlaylistLoader that signals 32885 // that the segments have changed in some way and use that to 32886 // update the SegmentLoader instead of doing it twice here and 32887 // on `loadedplaylist` 32888 32889 32890 this.mainSegmentLoader_.pause(); 32891 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 32892 32893 if (this.waitingForFastQualityPlaylistReceived_) { 32894 this.runFastQualitySwitch_(); 32895 } else { 32896 this.mainSegmentLoader_.load(); 32897 } 32898 32899 this.tech_.trigger({ 32900 type: 'mediachange', 32901 bubbles: true 32902 }); 32903 }); 32904 this.mainPlaylistLoader_.on('playlistunchanged', () => { 32905 const updatedPlaylist = this.mainPlaylistLoader_.media(); // ignore unchanged playlists that have already been 32906 // excluded for not-changing. We likely just have a really slowly updating 32907 // playlist. 32908 32909 if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') { 32910 return; 32911 } 32912 32913 const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist); 32914 32915 if (playlistOutdated) { 32916 // Playlist has stopped updating and we're stuck at its end. Try to 32917 // exclude it and switch to another playlist in the hope that that 32918 // one is updating (and give the player a chance to re-adjust to the 32919 // safe live point). 32920 this.excludePlaylist({ 32921 error: { 32922 message: 'Playlist no longer updating.', 32923 reason: 'playlist-unchanged' 32924 } 32925 }); // useful for monitoring QoS 32926 32927 this.tech_.trigger('playliststuck'); 32928 } 32929 }); 32930 this.mainPlaylistLoader_.on('renditiondisabled', () => { 32931 this.tech_.trigger({ 32932 type: 'usage', 32933 name: 'vhs-rendition-disabled' 32934 }); 32935 }); 32936 this.mainPlaylistLoader_.on('renditionenabled', () => { 32937 this.tech_.trigger({ 32938 type: 'usage', 32939 name: 'vhs-rendition-enabled' 32940 }); 32941 }); 32942 const playlistLoaderEvents = ['manifestrequeststart', 'manifestrequestcomplete', 'manifestparsestart', 'manifestparsecomplete', 'playlistrequeststart', 'playlistrequestcomplete', 'playlistparsestart', 'playlistparsecomplete', 'renditiondisabled', 'renditionenabled']; 32943 playlistLoaderEvents.forEach(eventName => { 32944 this.mainPlaylistLoader_.on(eventName, metadata => { 32945 // trigger directly on the player to ensure early events are fired. 32946 this.player_.trigger(_extends({}, metadata)); 32947 }); 32948 }); 32949 } 32950 /** 32951 * Given an updated media playlist (whether it was loaded for the first time, or 32952 * refreshed for live playlists), update any relevant properties and state to reflect 32953 * changes in the media that should be accounted for (e.g., cues and duration). 32954 * 32955 * @param {Object} updatedPlaylist the updated media playlist object 32956 * 32957 * @private 32958 */ 32959 32960 32961 handleUpdatedMediaPlaylist(updatedPlaylist) { 32962 if (this.useCueTags_) { 32963 this.updateAdCues_(updatedPlaylist); 32964 } // TODO: Create a new event on the PlaylistLoader that signals 32965 // that the segments have changed in some way and use that to 32966 // update the SegmentLoader instead of doing it twice here and 32967 // on `mediachange` 32968 32969 32970 this.mainSegmentLoader_.pause(); 32971 this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_); 32972 32973 if (this.waitingForFastQualityPlaylistReceived_) { 32974 this.runFastQualitySwitch_(); 32975 } 32976 32977 this.updateDuration(!updatedPlaylist.endList); // If the player isn't paused, ensure that the segment loader is running, 32978 // as it is possible that it was temporarily stopped while waiting for 32979 // a playlist (e.g., in case the playlist errored and we re-requested it). 32980 32981 if (!this.tech_.paused()) { 32982 this.mainSegmentLoader_.load(); 32983 32984 if (this.audioSegmentLoader_) { 32985 this.audioSegmentLoader_.load(); 32986 } 32987 } 32988 } 32989 /** 32990 * A helper function for triggerring presence usage events once per source 32991 * 32992 * @private 32993 */ 32994 32995 32996 triggerPresenceUsage_(main, media) { 32997 const mediaGroups = main.mediaGroups || {}; 32998 let defaultDemuxed = true; 32999 const audioGroupKeys = Object.keys(mediaGroups.AUDIO); 33000 33001 for (const mediaGroup in mediaGroups.AUDIO) { 33002 for (const label in mediaGroups.AUDIO[mediaGroup]) { 33003 const properties = mediaGroups.AUDIO[mediaGroup][label]; 33004 33005 if (!properties.uri) { 33006 defaultDemuxed = false; 33007 } 33008 } 33009 } 33010 33011 if (defaultDemuxed) { 33012 this.tech_.trigger({ 33013 type: 'usage', 33014 name: 'vhs-demuxed' 33015 }); 33016 } 33017 33018 if (Object.keys(mediaGroups.SUBTITLES).length) { 33019 this.tech_.trigger({ 33020 type: 'usage', 33021 name: 'vhs-webvtt' 33022 }); 33023 } 33024 33025 if (Vhs$1.Playlist.isAes(media)) { 33026 this.tech_.trigger({ 33027 type: 'usage', 33028 name: 'vhs-aes' 33029 }); 33030 } 33031 33032 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 33033 this.tech_.trigger({ 33034 type: 'usage', 33035 name: 'vhs-alternate-audio' 33036 }); 33037 } 33038 33039 if (this.useCueTags_) { 33040 this.tech_.trigger({ 33041 type: 'usage', 33042 name: 'vhs-playlist-cue-tags' 33043 }); 33044 } 33045 } 33046 33047 shouldSwitchToMedia_(nextPlaylist) { 33048 const currentPlaylist = this.mainPlaylistLoader_.media() || this.mainPlaylistLoader_.pendingMedia_; 33049 const currentTime = this.tech_.currentTime(); 33050 const bufferLowWaterLine = this.bufferLowWaterLine(); 33051 const bufferHighWaterLine = this.bufferHighWaterLine(); 33052 const buffered = this.tech_.buffered(); 33053 return shouldSwitchToMedia({ 33054 buffered, 33055 currentTime, 33056 currentPlaylist, 33057 nextPlaylist, 33058 bufferLowWaterLine, 33059 bufferHighWaterLine, 33060 duration: this.duration(), 33061 bufferBasedABR: this.bufferBasedABR, 33062 log: this.logger_ 33063 }); 33064 } 33065 /** 33066 * Register event handlers on the segment loaders. A helper function 33067 * for construction time. 33068 * 33069 * @private 33070 */ 33071 33072 33073 setupSegmentLoaderListeners_() { 33074 this.mainSegmentLoader_.on('bandwidthupdate', () => { 33075 // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's 33076 // useful to check to see if a rendition switch should be made. 33077 this.checkABR_('bandwidthupdate'); 33078 this.tech_.trigger('bandwidthupdate'); 33079 }); 33080 this.mainSegmentLoader_.on('timeout', () => { 33081 if (this.bufferBasedABR) { 33082 // If a rendition change is needed, then it would've be done on `bandwidthupdate`. 33083 // Here the only consideration is that for buffer based ABR there's no guarantee 33084 // of an immediate switch (since the bandwidth is averaged with a timeout 33085 // bandwidth value of 1), so force a load on the segment loader to keep it going. 33086 this.mainSegmentLoader_.load(); 33087 } 33088 }); // `progress` events are not reliable enough of a bandwidth measure to trigger buffer 33089 // based ABR. 33090 33091 if (!this.bufferBasedABR) { 33092 this.mainSegmentLoader_.on('progress', () => { 33093 this.trigger('progress'); 33094 }); 33095 } 33096 33097 this.mainSegmentLoader_.on('error', () => { 33098 const error = this.mainSegmentLoader_.error(); 33099 this.excludePlaylist({ 33100 playlistToExclude: error.playlist, 33101 error 33102 }); 33103 }); 33104 this.mainSegmentLoader_.on('appenderror', () => { 33105 this.error = this.mainSegmentLoader_.error_; 33106 this.trigger('error'); 33107 }); 33108 this.mainSegmentLoader_.on('syncinfoupdate', () => { 33109 this.onSyncInfoUpdate_(); 33110 }); 33111 this.mainSegmentLoader_.on('timestampoffset', () => { 33112 this.tech_.trigger({ 33113 type: 'usage', 33114 name: 'vhs-timestamp-offset' 33115 }); 33116 }); 33117 this.audioSegmentLoader_.on('syncinfoupdate', () => { 33118 this.onSyncInfoUpdate_(); 33119 }); 33120 this.audioSegmentLoader_.on('appenderror', () => { 33121 this.error = this.audioSegmentLoader_.error_; 33122 this.trigger('error'); 33123 }); 33124 this.mainSegmentLoader_.on('ended', () => { 33125 this.logger_('main segment loader ended'); 33126 this.onEndOfStream(); 33127 }); // There is the possibility of the video segment and the audio segment 33128 // at a current time to be on different timelines. When this occurs, the player 33129 // forwards playback to a point where these two segment types are back on the same 33130 // timeline. This time will be just after the end of the audio segment that is on 33131 // a previous timeline. 33132 33133 this.timelineChangeController_.on('audioTimelineBehind', () => { 33134 const segmentInfo = this.audioSegmentLoader_.pendingSegment_; 33135 33136 if (!segmentInfo || !segmentInfo.segment || !segmentInfo.segment.syncInfo) { 33137 return; 33138 } // Update the current time to just after the faulty audio segment. 33139 // This moves playback to a spot where both audio and video segments 33140 // are on the same timeline. 33141 33142 33143 const newTime = segmentInfo.segment.syncInfo.end + 0.01; 33144 this.tech_.setCurrentTime(newTime); 33145 }); 33146 this.timelineChangeController_.on('fixBadTimelineChange', () => { 33147 // pause, reset-everything and load for all segment-loaders 33148 this.logger_('Fix bad timeline change. Restarting al segment loaders...'); 33149 this.mainSegmentLoader_.pause(); 33150 this.mainSegmentLoader_.resetEverything(); 33151 33152 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33153 this.audioSegmentLoader_.pause(); 33154 this.audioSegmentLoader_.resetEverything(); 33155 } 33156 33157 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 33158 this.subtitleSegmentLoader_.pause(); 33159 this.subtitleSegmentLoader_.resetEverything(); 33160 } // start segment loader loading in case they are paused 33161 33162 33163 this.load(); 33164 }); 33165 this.mainSegmentLoader_.on('earlyabort', event => { 33166 // never try to early abort with the new ABR algorithm 33167 if (this.bufferBasedABR) { 33168 return; 33169 } 33170 33171 this.delegateLoaders_('all', ['abort']); 33172 this.excludePlaylist({ 33173 error: { 33174 message: 'Aborted early because there isn\'t enough bandwidth to complete ' + 'the request without rebuffering.' 33175 }, 33176 playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS 33177 }); 33178 }); 33179 33180 const updateCodecs = () => { 33181 if (!this.sourceUpdater_.hasCreatedSourceBuffers()) { 33182 return this.tryToCreateSourceBuffers_(); 33183 } 33184 33185 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 33186 33187 if (!codecs) { 33188 return; 33189 } 33190 33191 this.sourceUpdater_.addOrChangeSourceBuffers(codecs); 33192 }; 33193 33194 this.mainSegmentLoader_.on('trackinfo', updateCodecs); 33195 this.audioSegmentLoader_.on('trackinfo', updateCodecs); 33196 this.mainSegmentLoader_.on('fmp4', () => { 33197 if (!this.triggeredFmp4Usage) { 33198 this.tech_.trigger({ 33199 type: 'usage', 33200 name: 'vhs-fmp4' 33201 }); 33202 this.triggeredFmp4Usage = true; 33203 } 33204 }); 33205 this.audioSegmentLoader_.on('fmp4', () => { 33206 if (!this.triggeredFmp4Usage) { 33207 this.tech_.trigger({ 33208 type: 'usage', 33209 name: 'vhs-fmp4' 33210 }); 33211 this.triggeredFmp4Usage = true; 33212 } 33213 }); 33214 this.audioSegmentLoader_.on('ended', () => { 33215 this.logger_('audioSegmentLoader ended'); 33216 this.onEndOfStream(); 33217 }); 33218 const segmentLoaderEvents = ['segmentselected', 'segmentloadstart', 'segmentloaded', 'segmentkeyloadstart', 'segmentkeyloadcomplete', 'segmentdecryptionstart', 'segmentdecryptioncomplete', 'segmenttransmuxingstart', 'segmenttransmuxingcomplete', 'segmenttransmuxingtrackinfoavailable', 'segmenttransmuxingtiminginfoavailable', 'segmentappendstart', 'appendsdone', 'bandwidthupdated', 'timelinechange', 'codecschange']; 33219 segmentLoaderEvents.forEach(eventName => { 33220 this.mainSegmentLoader_.on(eventName, metadata => { 33221 this.player_.trigger(_extends({}, metadata)); 33222 }); 33223 this.audioSegmentLoader_.on(eventName, metadata => { 33224 this.player_.trigger(_extends({}, metadata)); 33225 }); 33226 this.subtitleSegmentLoader_.on(eventName, metadata => { 33227 this.player_.trigger(_extends({}, metadata)); 33228 }); 33229 }); 33230 } 33231 33232 mediaSecondsLoaded_() { 33233 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded); 33234 } 33235 /** 33236 * Call load on our SegmentLoaders 33237 */ 33238 33239 33240 load() { 33241 this.mainSegmentLoader_.load(); 33242 33243 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33244 this.audioSegmentLoader_.load(); 33245 } 33246 33247 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 33248 this.subtitleSegmentLoader_.load(); 33249 } 33250 } 33251 /** 33252 * Call pause on our SegmentLoaders 33253 */ 33254 33255 33256 pause() { 33257 this.mainSegmentLoader_.pause(); 33258 33259 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33260 this.audioSegmentLoader_.pause(); 33261 } 33262 33263 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 33264 this.subtitleSegmentLoader_.pause(); 33265 } 33266 } 33267 /** 33268 * Re-tune playback quality level for the current player 33269 * conditions. This method will perform destructive actions like removing 33270 * already buffered content in order to readjust the currently active 33271 * playlist quickly. This is good for manual quality changes 33272 * 33273 * @private 33274 */ 33275 33276 33277 fastQualityChange_(media = this.selectPlaylist()) { 33278 if (media && media === this.mainPlaylistLoader_.media()) { 33279 this.logger_('skipping fastQualityChange because new media is same as old'); 33280 return; 33281 } 33282 33283 this.switchMedia_(media, 'fast-quality'); // we would like to avoid race condition when we call fastQuality, 33284 // reset everything and start loading segments from prev segments instead of new because new playlist is not received yet 33285 33286 this.waitingForFastQualityPlaylistReceived_ = true; 33287 } 33288 33289 runFastQualitySwitch_() { 33290 this.waitingForFastQualityPlaylistReceived_ = false; 33291 this.mainSegmentLoader_.pause(); 33292 this.mainSegmentLoader_.resetEverything(); 33293 33294 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33295 this.audioSegmentLoader_.pause(); 33296 this.audioSegmentLoader_.resetEverything(); 33297 } 33298 33299 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 33300 this.subtitleSegmentLoader_.pause(); 33301 this.subtitleSegmentLoader_.resetEverything(); 33302 } // start segment loader loading in case they are paused 33303 33304 33305 this.load(); 33306 } 33307 /** 33308 * Begin playback. 33309 */ 33310 33311 33312 play() { 33313 if (this.setupFirstPlay()) { 33314 return; 33315 } 33316 33317 if (this.tech_.ended()) { 33318 this.tech_.setCurrentTime(0); 33319 } 33320 33321 if (this.hasPlayed_) { 33322 this.load(); 33323 } 33324 33325 const seekable = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window, 33326 // seek forward to the live point 33327 33328 if (this.tech_.duration() === Infinity) { 33329 if (this.tech_.currentTime() < seekable.start(0)) { 33330 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1)); 33331 } 33332 } 33333 } 33334 /** 33335 * Seek to the latest media position if this is a live video and the 33336 * player and video are loaded and initialized. 33337 */ 33338 33339 33340 setupFirstPlay() { 33341 const media = this.mainPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play 33342 // If 1) there is no active media 33343 // 2) the player is paused 33344 // 3) the first play has already been setup 33345 // then exit early 33346 33347 if (!media || this.tech_.paused() || this.hasPlayed_) { 33348 return false; 33349 } // when the video is a live stream and/or has a start time 33350 33351 33352 if (!media.endList || media.start) { 33353 const seekable = this.seekable(); 33354 33355 if (!seekable.length) { 33356 // without a seekable range, the player cannot seek to begin buffering at the 33357 // live or start point 33358 return false; 33359 } 33360 33361 const seekableEnd = seekable.end(0); 33362 let startPoint = seekableEnd; 33363 33364 if (media.start) { 33365 const offset = media.start.timeOffset; 33366 33367 if (offset < 0) { 33368
33368startPoint = Math.max(seekableEnd + offset
vendor: 82,959 bytes, lines 33368-35649
33368, seekable.start(0)); 33369 } else { 33370 startPoint = Math.min(seekableEnd, offset); 33371 } 33372 } // trigger firstplay to inform the source handler to ignore the next seek event 33373 33374 33375 this.trigger('firstplay'); // seek to the live point 33376 33377 this.tech_.setCurrentTime(startPoint); 33378 } 33379 33380 this.hasPlayed_ = true; // we can begin loading now that everything is ready 33381 33382 this.load(); 33383 return true; 33384 } 33385 /** 33386 * handle the sourceopen event on the MediaSource 33387 * 33388 * @private 33389 */ 33390 33391 33392 handleSourceOpen_() { 33393 // Only attempt to create the source buffer if none already exist. 33394 // handleSourceOpen is also called when we are "re-opening" a source buffer 33395 // after `endOfStream` has been called (in response to a seek for instance) 33396 this.tryToCreateSourceBuffers_(); // if autoplay is enabled, begin playback. This is duplicative of 33397 // code in video.js but is required because play() must be invoked 33398 // *after* the media source has opened. 33399 33400 if (this.tech_.autoplay()) { 33401 const playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request 33402 // on browsers which return a promise 33403 33404 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 33405 playPromise.then(null, e => {}); 33406 } 33407 } 33408 33409 this.trigger('sourceopen'); 33410 } 33411 /** 33412 * handle the sourceended event on the MediaSource 33413 * 33414 * @private 33415 */ 33416 33417 33418 handleSourceEnded_() { 33419 if (!this.inbandTextTracks_.metadataTrack_) { 33420 return; 33421 } 33422 33423 const cues = this.inbandTextTracks_.metadataTrack_.cues; 33424 33425 if (!cues || !cues.length) { 33426 return; 33427 } 33428 33429 const duration = this.duration(); 33430 cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ? Number.MAX_VALUE : duration; 33431 } 33432 /** 33433 * handle the durationchange event on the MediaSource 33434 * 33435 * @private 33436 */ 33437 33438 33439 handleDurationChange_() { 33440 this.tech_.trigger('durationchange'); 33441 } 33442 /** 33443 * Calls endOfStream on the media source when all active stream types have called 33444 * endOfStream 33445 * 33446 * @param {string} streamType 33447 * Stream type of the segment loader that called endOfStream 33448 * @private 33449 */ 33450 33451 33452 onEndOfStream() { 33453 let isEndOfStream = this.mainSegmentLoader_.ended_; 33454 33455 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33456 const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_(); // if the audio playlist loader exists, then alternate audio is active 33457 33458 if (!mainMediaInfo || mainMediaInfo.hasVideo) { 33459 // if we do not know if the main segment loader contains video yet or if we 33460 // definitively know the main segment loader contains video, then we need to wait 33461 // for both main and audio segment loaders to call endOfStream 33462 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 33463 } else { 33464 // otherwise just rely on the audio loader 33465 isEndOfStream = this.audioSegmentLoader_.ended_; 33466 } 33467 } 33468 33469 if (!isEndOfStream) { 33470 return; 33471 } 33472 33473 this.stopABRTimer_(); 33474 this.sourceUpdater_.endOfStream(); 33475 } 33476 /** 33477 * Check if a playlist has stopped being updated 33478 * 33479 * @param {Object} playlist the media playlist object 33480 * @return {boolean} whether the playlist has stopped being updated or not 33481 */ 33482 33483 33484 stuckAtPlaylistEnd_(playlist) { 33485 const seekable = this.seekable(); 33486 33487 if (!seekable.length) { 33488 // playlist doesn't have enough information to determine whether we are stuck 33489 return false; 33490 } 33491 33492 const expired = this.syncController_.getExpiredTime(playlist, this.duration()); 33493 33494 if (expired === null) { 33495 return false; 33496 } // does not use the safe live end to calculate playlist end, since we 33497 // don't want to say we are stuck while there is still content 33498 33499 33500 const absolutePlaylistEnd = Vhs$1.Playlist.playlistEnd(playlist, expired); 33501 const currentTime = this.tech_.currentTime(); 33502 const buffered = this.tech_.buffered(); 33503 33504 if (!buffered.length) { 33505 // return true if the playhead reached the absolute end of the playlist 33506 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 33507 } 33508 33509 const bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute 33510 // end of playlist 33511 33512 return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA; 33513 } 33514 /** 33515 * Exclude a playlist for a set amount of time, making it unavailable for selection by 33516 * the rendition selection algorithm, then force a new playlist (rendition) selection. 33517 * 33518 * @param {Object=} playlistToExclude 33519 * the playlist to exclude, defaults to the currently selected playlist 33520 * @param {Object=} error 33521 * an optional error 33522 * @param {number=} playlistExclusionDuration 33523 * an optional number of seconds to exclude the playlist 33524 */ 33525 33526 33527 excludePlaylist({ 33528 playlistToExclude = this.mainPlaylistLoader_.media(), 33529 error = {}, 33530 playlistExclusionDuration 33531 }) { 33532 // If the `error` was generated by the playlist loader, it will contain 33533 // the playlist we were trying to load (but failed) and that should be 33534 // excluded instead of the currently selected playlist which is likely 33535 // out-of-date in this scenario 33536 playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media(); 33537 playlistExclusionDuration = playlistExclusionDuration || error.playlistExclusionDuration || this.playlistExclusionDuration; // If there is no current playlist, then an error occurred while we were 33538 // trying to load the main OR while we were disposing of the tech 33539 33540 if (!playlistToExclude) { 33541 this.error = error; 33542 33543 if (this.mediaSource.readyState !== 'open') { 33544 this.trigger('error'); 33545 } else { 33546 this.sourceUpdater_.endOfStream('network'); 33547 } 33548 33549 return; 33550 } 33551 33552 playlistToExclude.playlistErrors_++; 33553 const playlists = this.mainPlaylistLoader_.main.playlists; 33554 const enabledPlaylists = playlists.filter(isEnabled); 33555 const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded 33556 // forever 33557 33558 if (playlists.length === 1 && playlistExclusionDuration !== Infinity) { 33559 videojs__default["default"].log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` + 'Trying again since it is the only playlist.'); 33560 this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay 33561 33562 return this.mainPlaylistLoader_.load(isFinalRendition); 33563 } 33564 33565 if (isFinalRendition) { 33566 // If we're content steering, try other pathways. 33567 if (this.main().contentSteering) { 33568 const pathway = this.pathwayAttribute_(playlistToExclude); // Ignore at least 1 steering manifest refresh. 33569 33570 const reIncludeDelay = this.contentSteeringController_.steeringManifest.ttl * 1000; 33571 this.contentSteeringController_.excludePathway(pathway); 33572 this.excludeThenChangePathway_(); 33573 setTimeout(() => { 33574 this.contentSteeringController_.addAvailablePathway(pathway); 33575 }, reIncludeDelay); 33576 return; 33577 } // Since we're on the final non-excluded playlist, and we're about to exclude 33578 // it, instead of erring the player or retrying this playlist, clear out the current 33579 // exclusion list. This allows other playlists to be attempted in case any have been 33580 // fixed. 33581 33582 33583 let reincluded = false; 33584 playlists.forEach(playlist => { 33585 // skip current playlist which is about to be excluded 33586 if (playlist === playlistToExclude) { 33587 return; 33588 } 33589 33590 const excludeUntil = playlist.excludeUntil; // a playlist cannot be reincluded if it wasn't excluded to begin with. 33591 33592 if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) { 33593 reincluded = true; 33594 delete playlist.excludeUntil; 33595 } 33596 }); 33597 33598 if (reincluded) { 33599 videojs__default["default"].log.warn('Removing other playlists from the exclusion list because the last ' + 'rendition is about to be excluded.'); // Technically we are retrying a playlist, in that we are simply retrying a previous 33600 // playlist. This is needed for users relying on the retryplaylist event to catch a 33601 // case where the player might be stuck and looping through "dead" playlists. 33602 33603 this.tech_.trigger('retryplaylist'); 33604 } 33605 } // Exclude this playlist 33606 33607 33608 let excludeUntil; 33609 33610 if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) { 33611 excludeUntil = Infinity; 33612 } else { 33613 excludeUntil = Date.now() + playlistExclusionDuration * 1000; 33614 } 33615 33616 playlistToExclude.excludeUntil = excludeUntil; 33617 33618 if (error.reason) { 33619 playlistToExclude.lastExcludeReason_ = error.reason; 33620 } 33621 33622 this.tech_.trigger('excludeplaylist'); 33623 this.tech_.trigger({ 33624 type: 'usage', 33625 name: 'vhs-rendition-excluded' 33626 }); // TODO: only load a new playlist if we're excluding the current playlist 33627 // If this function was called with a playlist that's not the current active playlist 33628 // (e.g., media().id !== playlistToExclude.id), 33629 // then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist. 33630 33631 const nextPlaylist = this.selectPlaylist(); 33632 33633 if (!nextPlaylist) { 33634 this.error = 'Playback cannot continue. No available working or supported playlists.'; 33635 this.trigger('error'); 33636 return; 33637 } 33638 33639 const logFn = error.internal ? this.logger_ : videojs__default["default"].log.warn; 33640 const errorMessage = error.message ? ' ' + error.message : ''; 33641 logFn(`${error.internal ? 'Internal problem' : 'Problem'} encountered with playlist ${playlistToExclude.id}.` + `${errorMessage} Switching to playlist ${nextPlaylist.id}.`); // if audio group changed reset audio loaders 33642 33643 if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) { 33644 this.delegateLoaders_('audio', ['abort', 'pause']); 33645 } // if subtitle group changed reset subtitle loaders 33646 33647 33648 if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) { 33649 this.delegateLoaders_('subtitle', ['abort', 'pause']); 33650 } 33651 33652 this.delegateLoaders_('main', ['abort', 'pause']); 33653 const delayDuration = nextPlaylist.targetDuration / 2 * 1000 || 5 * 1000; 33654 const shouldDelay = typeof nextPlaylist.lastRequest === 'number' && Date.now() - nextPlaylist.lastRequest <= delayDuration; // delay if it's a final rendition or if the last refresh is sooner than half targetDuration 33655 33656 return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay); 33657 } 33658 /** 33659 * Pause all segment/playlist loaders 33660 */ 33661 33662 33663 pauseLoading() { 33664 this.delegateLoaders_('all', ['abort', 'pause']); 33665 this.stopABRTimer_(); 33666 } 33667 /** 33668 * Call a set of functions in order on playlist loaders, segment loaders, 33669 * or both types of loaders. 33670 * 33671 * @param {string} filter 33672 * Filter loaders that should call fnNames using a string. Can be: 33673 * * all - run on all loaders 33674 * * audio - run on all audio loaders 33675 * * subtitle - run on all subtitle loaders 33676 * * main - run on the main loaders 33677 * 33678 * @param {Array|string} fnNames 33679 * A string or array of function names to call. 33680 */ 33681 33682 33683 delegateLoaders_(filter, fnNames) { 33684 const loaders = []; 33685 const dontFilterPlaylist = filter === 'all'; 33686 33687 if (dontFilterPlaylist || filter === 'main') { 33688 loaders.push(this.mainPlaylistLoader_); 33689 } 33690 33691 const mediaTypes = []; 33692 33693 if (dontFilterPlaylist || filter === 'audio') { 33694 mediaTypes.push('AUDIO'); 33695 } 33696 33697 if (dontFilterPlaylist || filter === 'subtitle') { 33698 mediaTypes.push('CLOSED-CAPTIONS'); 33699 mediaTypes.push('SUBTITLES'); 33700 } 33701 33702 mediaTypes.forEach(mediaType => { 33703 const loader = this.mediaTypes_[mediaType] && this.mediaTypes_[mediaType].activePlaylistLoader; 33704 33705 if (loader) { 33706 loaders.push(loader); 33707 } 33708 }); 33709 ['main', 'audio', 'subtitle'].forEach(name => { 33710 const loader = this[`${name}SegmentLoader_`]; 33711 33712 if (loader && (filter === name || filter === 'all')) { 33713 loaders.push(loader); 33714 } 33715 }); 33716 loaders.forEach(loader => fnNames.forEach(fnName => { 33717 if (typeof loader[fnName] === 'function') { 33718 loader[fnName](); 33719 } 33720 })); 33721 } 33722 /** 33723 * set the current time on all segment loaders 33724 * 33725 * @param {TimeRange} currentTime the current time to set 33726 * @return {TimeRange} the current time 33727 */ 33728 33729 33730 setCurrentTime(currentTime) { 33731 const buffered = findRange(this.tech_.buffered(), currentTime); 33732 33733 if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) { 33734 // return immediately if the metadata is not ready yet 33735 return 0; 33736 } // it's clearly an edge-case but don't thrown an error if asked to 33737 // seek within an empty playlist 33738 33739 33740 if (!this.mainPlaylistLoader_.media().segments) { 33741 return 0; 33742 } // if the seek location is already buffered, continue buffering as usual 33743 33744 33745 if (buffered && buffered.length) { 33746 return currentTime; 33747 } // cancel outstanding requests so we begin buffering at the new 33748 // location 33749 33750 33751 this.mainSegmentLoader_.pause(); 33752 this.mainSegmentLoader_.resetEverything(); 33753 33754 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33755 this.audioSegmentLoader_.pause(); 33756 this.audioSegmentLoader_.resetEverything(); 33757 } 33758 33759 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 33760 this.subtitleSegmentLoader_.pause(); 33761 this.subtitleSegmentLoader_.resetEverything(); 33762 } // start segment loader loading in case they are paused 33763 33764 33765 this.load(); 33766 } 33767 /** 33768 * get the current duration 33769 * 33770 * @return {TimeRange} the duration 33771 */ 33772 33773 33774 duration() { 33775 if (!this.mainPlaylistLoader_) { 33776 return 0; 33777 } 33778 33779 const media = this.mainPlaylistLoader_.media(); 33780 33781 if (!media) { 33782 // no playlists loaded yet, so can't determine a duration 33783 return 0; 33784 } // Don't rely on the media source for duration in the case of a live playlist since 33785 // setting the native MediaSource's duration to infinity ends up with consequences to 33786 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 33787 // 33788 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 33789 // however, few browsers have support for setLiveSeekableRange() 33790 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 33791 // 33792 // Until a time when the duration of the media source can be set to infinity, and a 33793 // seekable range specified across browsers, just return Infinity. 33794 33795 33796 if (!media.endList) { 33797 return Infinity; 33798 } // Since this is a VOD video, it is safe to rely on the media source's duration (if 33799 // available). If it's not available, fall back to a playlist-calculated estimate. 33800 33801 33802 if (this.mediaSource) { 33803 return this.mediaSource.duration; 33804 } 33805 33806 return Vhs$1.Playlist.duration(media); 33807 } 33808 /** 33809 * check the seekable range 33810 * 33811 * @return {TimeRange} the seekable range 33812 */ 33813 33814 33815 seekable() { 33816 return this.seekable_; 33817 } 33818 33819 getSeekableRange_(playlistLoader, mediaType) { 33820 const media = playlistLoader.media(); 33821 33822 if (!media) { 33823 return null; 33824 } 33825 33826 const mediaSequenceSync = this.syncController_.getMediaSequenceSync(mediaType); 33827 33828 if (mediaSequenceSync && mediaSequenceSync.isReliable) { 33829 const start = mediaSequenceSync.start; 33830 const end = mediaSequenceSync.end; 33831 33832 if (!isFinite(start) || !isFinite(end)) { 33833 return null; 33834 } 33835 33836 const liveEdgeDelay = Vhs$1.Playlist.liveEdgeDelay(this.mainPlaylistLoader_.main, media); // Make sure our seekable end is not less than the seekable start 33837 33838 const calculatedEnd = Math.max(start, end - liveEdgeDelay); 33839 return createTimeRanges([[start, calculatedEnd]]); 33840 } 33841 33842 const expired = this.syncController_.getExpiredTime(media, this.duration()); 33843 33844 if (expired === null) { 33845 return null; 33846 } 33847 33848 const seekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(this.mainPlaylistLoader_.main, media)); 33849 return seekable.length ? seekable : null; 33850 } 33851 33852 computeFinalSeekable_(mainSeekable, audioSeekable) { 33853 if (!audioSeekable) { 33854 return mainSeekable; 33855 } 33856 33857 const mainStart = mainSeekable.start(0); 33858 const mainEnd = mainSeekable.end(0); 33859 const audioStart = audioSeekable.start(0); 33860 const audioEnd = audioSeekable.end(0); 33861 33862 if (audioStart > mainEnd || mainStart > audioEnd) { 33863 // Seekables are far apart, rely on main 33864 return mainSeekable; 33865 } // Return the overlapping seekable range 33866 33867 33868 return createTimeRanges([[Math.max(mainStart, audioStart), Math.min(mainEnd, audioEnd)]]); 33869 } 33870 33871 onSyncInfoUpdate_() { 33872 // TODO check for creation of both source buffers before updating seekable 33873 // 33874 // A fix was made to this function where a check for 33875 // this.sourceUpdater_.hasCreatedSourceBuffers 33876 // was added to ensure that both source buffers were created before seekable was 33877 // updated. However, it originally had a bug where it was checking for a true and 33878 // returning early instead of checking for false. Setting it to check for false to 33879 // return early though created other issues. A call to play() would check for seekable 33880 // end without verifying that a seekable range was present. In addition, even checking 33881 // for that didn't solve some issues, as handleFirstPlay is sometimes worked around 33882 // due to a media update calling load on the segment loaders, skipping a seek to live, 33883 // thereby starting live streams at the beginning of the stream rather than at the end. 33884 // 33885 // This conditional should be fixed to wait for the creation of two source buffers at 33886 // the same time as the other sections of code are fixed to properly seek to live and 33887 // not throw an error due to checking for a seekable end when no seekable range exists. 33888 // 33889 // For now, fall back to the older behavior, with the understanding that the seekable 33890 // range may not be completely correct, leading to a suboptimal initial live point. 33891 if (!this.mainPlaylistLoader_) { 33892 return; 33893 } 33894 33895 const mainSeekable = this.getSeekableRange_(this.mainPlaylistLoader_, 'main'); 33896 33897 if (!mainSeekable) { 33898 return; 33899 } 33900 33901 let audioSeekable; 33902 33903 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 33904 audioSeekable = this.getSeekableRange_(this.mediaTypes_.AUDIO.activePlaylistLoader, 'audio'); 33905 33906 if (!audioSeekable) { 33907 return; 33908 } 33909 } 33910 33911 const oldSeekable = this.seekable_; 33912 this.seekable_ = this.computeFinalSeekable_(mainSeekable, audioSeekable); 33913 33914 if (!this.seekable_) { 33915 return; 33916 } 33917 33918 if (oldSeekable && oldSeekable.length && this.seekable_.length) { 33919 if (oldSeekable.start(0) === this.seekable_.start(0) && oldSeekable.end(0) === this.seekable_.end(0)) { 33920 // Seekable range hasn't changed 33921 return; 33922 } 33923 } 33924 33925 this.logger_(`seekable updated [${printableRange(this.seekable_)}]`); 33926 const metadata = { 33927 seekableRanges: this.seekable_ 33928 }; 33929 this.trigger({ 33930 type: 'seekablerangeschanged', 33931 metadata 33932 }); 33933 this.tech_.trigger('seekablechanged'); 33934 } 33935 /** 33936 * Update the player duration 33937 */ 33938 33939 33940 updateDuration(isLive) { 33941 if (this.updateDuration_) { 33942 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 33943 this.updateDuration_ = null; 33944 } 33945 33946 if (this.mediaSource.readyState !== 'open') { 33947 this.updateDuration_ = this.updateDuration.bind(this, isLive); 33948 this.mediaSource.addEventListener('sourceopen', this.updateDuration_); 33949 return; 33950 } 33951 33952 if (isLive) { 33953 const seekable = this.seekable(); 33954 33955 if (!seekable.length) { 33956 return; 33957 } // Even in the case of a live playlist, the native MediaSource's duration should not 33958 // be set to Infinity (even though this would be expected for a live playlist), since 33959 // setting the native MediaSource's duration to infinity ends up with consequences to 33960 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 33961 // 33962 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 33963 // however, few browsers have support for setLiveSeekableRange() 33964 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 33965 // 33966 // Until a time when the duration of the media source can be set to infinity, and a 33967 // seekable range specified across browsers, the duration should be greater than or 33968 // equal to the last possible seekable value. 33969 // MediaSource duration starts as NaN 33970 // It is possible (and probable) that this case will never be reached for many 33971 // sources, since the MediaSource reports duration as the highest value without 33972 // accounting for timestamp offset. For example, if the timestamp offset is -100 and 33973 // we buffered times 0 to 100 with real times of 100 to 200, even though current 33974 // time will be between 0 and 100, the native media source may report the duration 33975 // as 200. However, since we report duration separate from the media source (as 33976 // Infinity), and as long as the native media source duration value is greater than 33977 // our reported seekable range, seeks will work as expected. The large number as 33978 // duration for live is actually a strategy used by some players to work around the 33979 // issue of live seekable ranges cited above. 33980 33981 33982 if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) { 33983 this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1)); 33984 } 33985 33986 return; 33987 } 33988 33989 const buffered = this.tech_.buffered(); 33990 let duration = Vhs$1.Playlist.duration(this.mainPlaylistLoader_.media()); 33991 33992 if (buffered.length > 0) { 33993 duration = Math.max(duration, buffered.end(buffered.length - 1)); 33994 } 33995 33996 if (this.mediaSource.duration !== duration) { 33997 this.sourceUpdater_.setDuration(duration); 33998 } 33999 } 34000 /** 34001 * dispose of the PlaylistController and everything 34002 * that it controls 34003 */ 34004 34005 34006 dispose() { 34007 this.trigger('dispose'); 34008 this.decrypter_.terminate(); 34009 this.mainPlaylistLoader_.dispose(); 34010 this.mainSegmentLoader_.dispose(); 34011 this.contentSteeringController_.dispose(); 34012 this.keyStatusMap_.clear(); 34013 34014 if (this.loadOnPlay_) { 34015 this.tech_.off('play', this.loadOnPlay_); 34016 } 34017 34018 ['AUDIO', 'SUBTITLES'].forEach(type => { 34019 const groups = this.mediaTypes_[type].groups; 34020 34021 for (const id in groups) { 34022 groups[id].forEach(group => { 34023 if (group.playlistLoader) { 34024 group.playlistLoader.dispose(); 34025 } 34026 }); 34027 } 34028 }); 34029 this.audioSegmentLoader_.dispose(); 34030 this.subtitleSegmentLoader_.dispose(); 34031 this.sourceUpdater_.dispose(); 34032 this.timelineChangeController_.dispose(); 34033 this.stopABRTimer_(); 34034 34035 if (this.updateDuration_) { 34036 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 34037 } 34038 34039 this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 34040 34041 this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_); 34042 this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_); 34043 this.off(); 34044 } 34045 /** 34046 * return the main playlist object if we have one 34047 * 34048 * @return {Object} the main playlist object that we parsed 34049 */ 34050 34051 34052 main() { 34053 return this.mainPlaylistLoader_.main; 34054 } 34055 /** 34056 * return the currently selected playlist 34057 * 34058 * @return {Object} the currently selected playlist object that we parsed 34059 */ 34060 34061 34062 media() { 34063 // playlist loader will not return media if it has not been fully loaded 34064 return this.mainPlaylistLoader_.media() || this.initialMedia_; 34065 } 34066 34067 areMediaTypesKnown_() { 34068 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 34069 const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_(); // if we are not using an audio loader, then we have audio media info 34070 // otherwise check on the segment loader. 34071 34072 const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_(); // one or both loaders has not loaded sufficently to get codecs 34073 34074 if (!hasMainMediaInfo || !hasAudioMediaInfo) { 34075 return false; 34076 } 34077 34078 return true; 34079 } // find from and to for codec switch event 34080 34081 34082 getCodecsOrExclude_() { 34083 const media = { 34084 main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {}, 34085 audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {} 34086 }; 34087 const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); // set "main" media equal to video 34088 34089 media.video = media.main; 34090 const playlistCodecs = codecsForPlaylist(this.main(), playlist); 34091 const codecs = {}; 34092 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 34093 34094 if (media.main.hasVideo) { 34095 codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC; 34096 } 34097 34098 if (media.main.isMuxed) { 34099 codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`; 34100 } 34101 34102 if (media.main.hasAudio && !media.main.isMuxed || media.audio.hasAudio || usingAudioLoader) { 34103 codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC; // set audio isFmp4 so we use the correct "supports" function below 34104 34105 media.audio.isFmp4 = media.main.hasAudio && !media.main.isMuxed ? media.main.isFmp4 : media.audio.isFmp4; 34106 } // no codecs, no playback. 34107 34108 34109 if (!codecs.audio && !codecs.video) { 34110 this.excludePlaylist({ 34111 playlistToExclude: playlist, 34112 error: { 34113 message: 'Could not determine codecs for playlist.' 34114 }, 34115 playlistExclusionDuration: Infinity 34116 }); 34117 return; 34118 } // fmp4 relies on browser support, while ts relies on muxer support 34119 34120 34121 const supportFunction = (isFmp4, codec) => isFmp4 ? browserSupportsCodec(codec, this.usingManagedMediaSource_) : muxerSupportsCodec(codec); 34122 34123 const unsupportedCodecs = {}; 34124 let unsupportedAudio; 34125 ['video', 'audio'].forEach(function (type) { 34126 if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) { 34127 const supporter = media[type].isFmp4 ? 'browser' : 'muxer'; 34128 unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || []; 34129 unsupportedCodecs[supporter].push(codecs[type]); 34130 34131 if (type === 'audio') { 34132 unsupportedAudio = supporter; 34133 } 34134 } 34135 }); 34136 34137 if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) { 34138 const audioGroup = playlist.attributes.AUDIO; 34139 this.main().playlists.forEach(variant => { 34140 const variantAudioGroup = variant.attributes && variant.attributes.AUDIO; 34141 34142 if (variantAudioGroup === audioGroup && variant !== playlist) { 34143 variant.excludeUntil = Infinity; 34144 } 34145 }); 34146 this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`); 34147 } // if we have any unsupported codecs exclude this playlist. 34148 34149 34150 if (Object.keys(unsupportedCodecs).length) { 34151 const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => { 34152 if (acc) { 34153 acc += ', '; 34154 } 34155 34156 acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`; 34157 return acc; 34158 }, '') + '.'; 34159 this.excludePlaylist({ 34160 playlistToExclude: playlist, 34161 error: { 34162 internal: true, 34163 message 34164 }, 34165 playlistExclusionDuration: Infinity 34166 }); 34167 return; 34168 } // check if codec switching is happening 34169 34170 34171 if (this.sourceUpdater_.hasCreatedSourceBuffers() && !this.sourceUpdater_.canChangeType()) { 34172 const switchMessages = []; 34173 ['video', 'audio'].forEach(type => { 34174 const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type; 34175 const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type; 34176 34177 if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) { 34178 switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`); 34179 } 34180 }); 34181 34182 if (switchMessages.length) { 34183 this.excludePlaylist({ 34184 playlistToExclude: playlist, 34185 error: { 34186 message: `Codec switching not supported: ${switchMessages.join(', ')}.`, 34187 internal: true 34188 }, 34189 playlistExclusionDuration: Infinity 34190 }); 34191 return; 34192 } 34193 } // TODO: when using the muxer shouldn't we just return 34194 // the codecs that the muxer outputs? 34195 34196 34197 return codecs; 34198 } 34199 /** 34200 * Create source buffers and exlude any incompatible renditions. 34201 * 34202 * @private 34203 */ 34204 34205 34206 tryToCreateSourceBuffers_() { 34207 // media source is not ready yet or sourceBuffers are already 34208 // created. 34209 if (this.mediaSource.readyState !== 'open' || this.sourceUpdater_.hasCreatedSourceBuffers()) { 34210 return; 34211 } 34212 34213 if (!this.areMediaTypesKnown_()) { 34214 return; 34215 } 34216 34217 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 34218 34219 if (!codecs) { 34220 return; 34221 } 34222 34223 this.sourceUpdater_.createSourceBuffers(codecs); 34224 const codecString = [codecs.video, codecs.audio].filter(Boolean).join(','); 34225 this.excludeIncompatibleVariants_(codecString); 34226 } 34227 /** 34228 * Excludes playlists with codecs that are unsupported by the muxer and browser. 34229 */ 34230 34231 34232 excludeUnsupportedVariants_() { 34233 const playlists = this.main().playlists; 34234 const ids = []; // TODO: why don't we have a property to loop through all 34235 // playlist? Why did we ever mix indexes and keys? 34236 34237 Object.keys(playlists).forEach(key => { 34238 const variant = playlists[key]; // check if we already processed this playlist. 34239 34240 if (ids.indexOf(variant.id) !== -1) { 34241 return; 34242 } 34243 34244 ids.push(variant.id); 34245 const codecs = codecsForPlaylist(this.main, variant); 34246 const unsupported = []; 34247 34248 if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio, this.usingManagedMediaSource_)) { 34249 unsupported.push(`audio codec ${codecs.audio}`); 34250 } 34251 34252 if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video, this.usingManagedMediaSource_)) { 34253 unsupported.push(`video codec ${codecs.video}`); 34254 } 34255 34256 if (codecs.text && codecs.text === 'stpp.ttml.im1t') { 34257 unsupported.push(`text codec ${codecs.text}`); 34258 } 34259 34260 if (unsupported.length) { 34261 variant.excludeUntil = Infinity; 34262 this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`); 34263 } 34264 }); 34265 } 34266 /** 34267 * Exclude playlists that are known to be codec or 34268 * stream-incompatible with the SourceBuffer configuration. For 34269 * instance, Media Source Extensions would cause the video element to 34270 * stall waiting for video data if you switched from a variant with 34271 * video and audio to an audio-only one. 34272 * 34273 * @param {Object} media a media playlist compatible with the current 34274 * set of SourceBuffers. Variants in the current main playlist that 34275 * do not appear to have compatible codec or stream configurations 34276 * will be excluded from the default playlist selection algorithm 34277 * indefinitely. 34278 * @private 34279 */ 34280 34281 34282 excludeIncompatibleVariants_(codecString) { 34283 const ids = []; 34284 const playlists = this.main().playlists; 34285 const codecs = unwrapCodecList(parseCodecs(codecString)); 34286 const codecCount_ = codecCount(codecs); 34287 const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null; 34288 const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null; 34289 Object.keys(playlists).forEach(key => { 34290 const variant = playlists[key]; // check if we already processed this playlist. 34291 // or it if it is already excluded forever. 34292 34293 if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) { 34294 return; 34295 } 34296 34297 ids.push(variant.id); 34298 const exclusionReasons = []; // get codecs from the playlist for this variant 34299 34300 const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant); 34301 const variantCodecCount = codecCount(variantCodecs); // if no codecs are listed, we cannot determine that this 34302 // variant is incompatible. Wait for mux.js to probe 34303 34304 if (!variantCodecs.audio && !variantCodecs.video) { 34305 return; 34306 } // TODO: we can support this by removing the 34307 // old media source and creating a new one, but it will take some work. 34308 // The number of streams cannot change 34309 34310 34311 if (variantCodecCount !== codecCount_) { 34312 exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`); 34313 } // only exclude playlists by codec change, if codecs cannot switch 34314 // during playback. 34315 34316 34317 if (!this.sourceUpdater_.canChangeType()) { 34318 const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null; 34319 const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null; // the video codec cannot change 34320 34321 if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) { 34322 exclusionReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`); 34323 } // the audio codec cannot change 34324 34325 34326 if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) { 34327 exclusionReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`); 34328 } 34329 } 34330 34331 if (exclusionReasons.length) { 34332 variant.excludeUntil = Infinity; 34333 this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`); 34334 } 34335 }); 34336 } 34337 34338 updateAdCues_(media) { 34339 let offset = 0; 34340 const seekable = this.seekable(); 34341 34342 if (seekable.length) { 34343 offset = seekable.start(0); 34344 } 34345 34346 updateAdCues(media, this.cueTagsTrack_, offset); 34347 } 34348 /** 34349 * Calculates the desired forward buffer length based on current time 34350 * 34351 * @return {number} Desired forward buffer length in seconds 34352 */ 34353 34354 34355 goalBufferLength() { 34356 const currentTime = this.tech_.currentTime(); 34357 const initial = Config.GOAL_BUFFER_LENGTH; 34358 const rate = Config.GOAL_BUFFER_LENGTH_RATE; 34359 const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 34360 return Math.min(initial + currentTime * rate, max); 34361 } 34362 /** 34363 * Calculates the desired buffer low water line based on current time 34364 * 34365 * @return {number} Desired buffer low water line in seconds 34366 */ 34367 34368 34369 bufferLowWaterLine() { 34370 const currentTime = this.tech_.currentTime(); 34371 const initial = Config.BUFFER_LOW_WATER_LINE; 34372 const rate = Config.BUFFER_LOW_WATER_LINE_RATE; 34373 const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 34374 const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE); 34375 return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max); 34376 } 34377 34378 bufferHighWaterLine() { 34379 return Config.BUFFER_HIGH_WATER_LINE; 34380 } 34381 34382 addDateRangesToTextTrack_(dateRanges) { 34383 createMetadataTrackIfNotExists(this.inbandTextTracks_, 'com.apple.streaming', this.tech_); 34384 addDateRangeMetadata({ 34385 inbandTextTracks: this.inbandTextTracks_, 34386 dateRanges 34387 }); 34388 } 34389 34390 addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) { 34391 const timestampOffset = this.sourceUpdater_.videoBuffer ? this.sourceUpdater_.videoTimestampOffset() : this.sourceUpdater_.audioTimestampOffset(); // There's potentially an issue where we could double add metadata if there's a muxed 34392 // audio/video source with a metadata track, and an alt audio with a metadata track. 34393 // However, this probably won't happen, and if it does it can be handled then. 34394 34395 createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_); 34396 addMetadata({ 34397 inbandTextTracks: this.inbandTextTracks_, 34398 metadataArray, 34399 timestampOffset, 34400 videoDuration 34401 }); 34402 } 34403 /** 34404 * Utility for getting the pathway or service location from an HLS or DASH playlist. 34405 * 34406 * @param {Object} playlist for getting pathway from. 34407 * @return the pathway attribute of a playlist 34408 */ 34409 34410 34411 pathwayAttribute_(playlist) { 34412 return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation; 34413 } 34414 /** 34415 * Initialize available pathways and apply the tag properties. 34416 */ 34417 34418 34419 initContentSteeringController_() { 34420 const main = this.main(); 34421 34422 if (!main.contentSteering) { 34423 return; 34424 } 34425 34426 for (const playlist of main.playlists) { 34427 this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist)); 34428 } 34429 34430 this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering); // request the steering manifest immediately if queryBeforeStart is set. 34431 34432 if (this.contentSteeringController_.queryBeforeStart) { 34433 // When queryBeforeStart is true, initial request should omit steering parameters. 34434 this.contentSteeringController_.requestSteeringManifest(true); 34435 return; 34436 } // otherwise start content steering after playback starts 34437 34438 34439 this.tech_.one('canplay', () => { 34440 this.contentSteeringController_.requestSteeringManifest(); 34441 }); 34442 } 34443 /** 34444 * Reset the content steering controller and re-init. 34445 */ 34446 34447 34448 resetContentSteeringController_() { 34449 this.contentSteeringController_.clearAvailablePathways(); 34450 this.contentSteeringController_.dispose(); 34451 this.initContentSteeringController_(); 34452 } 34453 /** 34454 * Attaches the listeners for content steering. 34455 */ 34456 34457 34458 attachContentSteeringListeners_() { 34459 this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this)); 34460 const contentSteeringEvents = ['contentsteeringloadstart', 'contentsteeringloadcomplete', 'contentsteeringparsed']; 34461 contentSteeringEvents.forEach(eventName => { 34462 this.contentSteeringController_.on(eventName, metadata => { 34463 this.trigger(_extends({}, metadata)); 34464 }); 34465 }); 34466 34467 if (this.sourceType_ === 'dash') { 34468 this.mainPlaylistLoader_.on('loadedplaylist', () => { 34469 const main = this.main(); // check if steering tag or pathways changed. 34470 34471 const didDashTagChange = this.contentSteeringController_.didDASHTagChange(main.uri, main.contentSteering); 34472 34473 const didPathwaysChange = () => { 34474 const availablePathways = this.contentSteeringController_.getAvailablePathways(); 34475 const newPathways = []; 34476 34477 for (const playlist of main.playlists) { 34478 const serviceLocation = playlist.attributes.serviceLocation; 34479 34480 if (serviceLocation) { 34481 newPathways.push(serviceLocation); 34482 34483 if (!availablePathways.has(serviceLocation)) { 34484 return true; 34485 } 34486 } 34487 } // If we have no new serviceLocations and previously had availablePathways 34488 34489 34490 if (!newPathways.length && availablePathways.size) { 34491 return true; 34492 } 34493 34494 return false; 34495 }; 34496 34497 if (didDashTagChange || didPathwaysChange()) { 34498 this.resetContentSteeringController_(); 34499 } 34500 }); 34501 } 34502 } 34503 /** 34504 * Simple exclude and change playlist logic for content steering. 34505 */ 34506 34507 34508 excludeThenChangePathway_() { 34509 const currentPathway = this.contentSteeringController_.getPathway(); 34510 34511 if (!currentPathway) { 34512 return; 34513 } 34514 34515 this.handlePathwayClones_(); 34516 const main = this.main(); 34517 const playlists = main.playlists; 34518 const ids = new Set(); 34519 let didEnablePlaylists = false; 34520 Object.keys(playlists).forEach(key => { 34521 const variant = playlists[key]; 34522 const pathwayId = this.pathwayAttribute_(variant); 34523 const differentPathwayId = pathwayId && currentPathway !== pathwayId; 34524 const steeringExclusion = variant.excludeUntil === Infinity && variant.lastExcludeReason_ === 'content-steering'; 34525 34526 if (steeringExclusion && !differentPathwayId) { 34527 delete variant.excludeUntil; 34528 delete variant.lastExcludeReason_; 34529 didEnablePlaylists = true; 34530 } 34531 34532 const noExcludeUntil = !variant.excludeUntil && variant.excludeUntil !== Infinity; 34533 const shouldExclude = !ids.has(variant.id) && differentPathwayId && noExcludeUntil; 34534 34535 if (!shouldExclude) { 34536 return; 34537 } 34538 34539 ids.add(variant.id); 34540 variant.excludeUntil = Infinity; 34541 variant.lastExcludeReason_ = 'content-steering'; // TODO: kind of spammy, maybe move this. 34542 34543 this.logger_(`excluding ${variant.id} for ${variant.lastExcludeReason_}`); 34544 }); 34545 34546 if (this.contentSteeringController_.manifestType_ === 'DASH') { 34547 Object.keys(this.mediaTypes_).forEach(key => { 34548 const type = this.mediaTypes_[key]; 34549 34550 if (type.activePlaylistLoader) { 34551 const currentPlaylist = type.activePlaylistLoader.media_; // Check if the current media playlist matches the current CDN 34552 34553 if (currentPlaylist && currentPlaylist.attributes.serviceLocation !== currentPathway) { 34554 didEnablePlaylists = true; 34555 } 34556 } 34557 }); 34558 } 34559 34560 if (didEnablePlaylists) { 34561 this.changeSegmentPathway_(); 34562 } 34563 } 34564 /** 34565 * Add, update, or delete playlists and media groups for 34566 * the pathway clones for HLS Content Steering. 34567 * 34568 * See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 34569 * 34570 * NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and 34571 * `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or 34572 * `STABLE-RENDITION-ID` values. 34573 */ 34574 34575 34576 handlePathwayClones_() { 34577 const main = this.main(); 34578 const playlists = main.playlists; 34579 const currentPathwayClones = this.contentSteeringController_.currentPathwayClones; 34580 const nextPathwayClones = this.contentSteeringController_.nextPathwayClones; 34581 const hasClones = currentPathwayClones && currentPathwayClones.size || nextPathwayClones && nextPathwayClones.size; 34582 34583 if (!hasClones) { 34584 return; 34585 } 34586 34587 for (const [id, clone] of currentPathwayClones.entries()) { 34588 const newClone = nextPathwayClones.get(id); // Delete the old pathway clone. 34589 34590 if (!newClone) { 34591 this.mainPlaylistLoader_.updateOrDeleteClone(clone); 34592 this.contentSteeringController_.excludePathway(id); 34593 } 34594 } 34595 34596 for (const [id, clone] of nextPathwayClones.entries()) { 34597 const oldClone = currentPathwayClones.get(id); // Create a new pathway if it is a new pathway clone object. 34598 34599 if (!oldClone) { 34600 const playlistsToClone = playlists.filter(p => { 34601 return p.attributes['PATHWAY-ID'] === clone['BASE-ID']; 34602 }); 34603 playlistsToClone.forEach(p => { 34604 this.mainPlaylistLoader_.addClonePathway(clone, p); 34605 }); 34606 this.contentSteeringController_.addAvailablePathway(id); 34607 continue; 34608 } // There have not been changes to the pathway clone object, so skip. 34609 34610 34611 if (this.equalPathwayClones_(oldClone, clone)) { 34612 continue; 34613 } // Update a preexisting cloned pathway. 34614 // True is set for the update flag. 34615 34616 34617 this.mainPlaylistLoader_.updateOrDeleteClone(clone, true); 34618 this.contentSteeringController_.addAvailablePathway(id); 34619 } // Deep copy contents of next to current pathways. 34620 34621 34622 this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones]))); 34623 } 34624 /** 34625 * Determines whether two pathway clone objects are equivalent. 34626 * 34627 * @param {Object} a The first pathway clone object. 34628 * @param {Object} b The second pathway clone object. 34629 * @return {boolean} True if the pathway clone objects are equal, false otherwise. 34630 */ 34631 34632 34633 equalPathwayClones_(a, b) { 34634 if (a['BASE-ID'] !== b['BASE-ID'] || a.ID !== b.ID || a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST) { 34635 return false; 34636 } 34637 34638 const aParams = a['URI-REPLACEMENT'].PARAMS; 34639 const bParams = b['URI-REPLACEMENT'].PARAMS; // We need to iterate through both lists of params because one could be 34640 // missing a parameter that the other has. 34641 34642 for (const p in aParams) { 34643 if (aParams[p] !== bParams[p]) { 34644 return false; 34645 } 34646 } 34647 34648 for (const p in bParams) { 34649 if (aParams[p] !== bParams[p]) { 34650 return false; 34651 } 34652 } 34653 34654 return true; 34655 } 34656 /** 34657 * Changes the current playlists for audio, video and subtitles after a new pathway 34658 * is chosen from content steering. 34659 */ 34660 34661 34662 changeSegmentPathway_() { 34663 const nextPlaylist = this.selectPlaylist(); 34664 this.pauseLoading(); // Switch audio and text track playlists if necessary in DASH 34665 34666 if (this.contentSteeringController_.manifestType_ === 'DASH') { 34667 this.switchMediaForDASHContentSteering_(); 34668 } 34669 34670 this.switchMedia_(nextPlaylist, 'content-steering'); 34671 } 34672 /** 34673 * Iterates through playlists and check their keyId set and compare with the 34674 * keyStatusMap, only enable playlists that have a usable key. If the playlist 34675 * has no keyId leave it enabled by default. 34676 */ 34677 34678 34679 excludeNonUsablePlaylistsByKeyId_() { 34680 if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) { 34681 return; 34682 } 34683 34684 let nonUsableKeyStatusCount = 0; 34685 const NON_USABLE = 'non-usable'; 34686 this.mainPlaylistLoader_.main.playlists.forEach(playlist => { 34687 const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); // If the playlist doesn't have keyIDs lets not exclude it. 34688 34689 if (!keyIdSet || !keyIdSet.size) { 34690 return; 34691 } 34692 34693 keyIdSet.forEach(key => { 34694 const USABLE = 'usable'; 34695 const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE; 34696 const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity; 34697 34698 if (!hasUsableKeyStatus) { 34699 // Only exclude playlists that haven't already been excluded as non-usable. 34700 if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) { 34701 playlist.excludeUntil = Infinity; 34702 playlist.lastExcludeReason_ = NON_USABLE; 34703 this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`); 34704 } // count all nonUsableKeyStatus 34705 34706 34707 nonUsableKeyStatusCount++; 34708 } else if (hasUsableKeyStatus && nonUsableExclusion) { 34709 delete playlist.excludeUntil; 34710 delete playlist.lastExcludeReason_; 34711 this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`); 34712 } 34713 }); 34714 }); // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe. 34715 34716 if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) { 34717 this.mainPlaylistLoader_.main.playlists.forEach(playlist => { 34718 const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720; 34719 const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE; 34720 34721 if (isNonHD && excludedForNonUsableKey) { 34722 // Only delete the excludeUntil so we don't try and re-exclude these playlists. 34723 delete playlist.excludeUntil; 34724 videojs__default["default"].log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`); 34725 } 34726 }); 34727 } 34728 } 34729 /** 34730 * Adds a keystatus to the keystatus map, tries to convert to string if necessary. 34731 * 34732 * @param {any} keyId the keyId to add a status for 34733 * @param {string} status the status of the keyId 34734 */ 34735 34736 34737 addKeyStatus_(keyId, status) { 34738 const isString = typeof keyId === 'string'; 34739 const keyIdHexString = isString ? keyId : bufferToHexString(keyId); 34740 const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase(); 34741 this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`); 34742 this.keyStatusMap_.set(formattedKeyIdString, status); 34743 } 34744 /** 34745 * Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists. 34746 * 34747 * @param {any} keyId the keyId from the keystatuschange event 34748 * @param {string} status the key status string 34749 */ 34750 34751 34752 updatePlaylistByKeyStatus(keyId, status) { 34753 this.addKeyStatus_(keyId, status); 34754 34755 if (!this.waitingForFastQualityPlaylistReceived_) { 34756 this.excludeNonUsableThenChangePlaylist_(); 34757 } // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated. 34758 34759 34760 this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); 34761 this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); 34762 } 34763 34764 excludeNonUsableThenChangePlaylist_() { 34765 this.excludeNonUsablePlaylistsByKeyId_(); 34766 this.fastQualityChange_(); 34767 } 34768 34769 } 34770 34771 /** 34772 * Returns a function that acts as the Enable/disable playlist function. 34773 * 34774 * @param {PlaylistLoader} loader - The main playlist loader 34775 * @param {string} playlistID - id of the playlist 34776 * @param {Function} changePlaylistFn - A function to be called after a 34777 * playlist's enabled-state has been changed. Will NOT be called if a 34778 * playlist's enabled-state is unchanged 34779 * @param {boolean=} enable - Value to set the playlist enabled-state to 34780 * or if undefined returns the current enabled-state for the playlist 34781 * @return {Function} Function for setting/getting enabled 34782 */ 34783 34784 const enableFunction = (loader, playlistID, changePlaylistFn) => enable => { 34785 const playlist = loader.main.playlists[playlistID]; 34786 const incompatible = isIncompatible(playlist); 34787 const currentlyEnabled = isEnabled(playlist); 34788 34789 if (typeof enable === 'undefined') { 34790 return currentlyEnabled; 34791 } 34792 34793 if (enable) { 34794 delete playlist.disabled; 34795 } else { 34796 playlist.disabled = true; 34797 } 34798 34799 const metadata = { 34800 renditionInfo: { 34801 id: playlistID, 34802 bandwidth: playlist.attributes.BANDWIDTH, 34803 resolution: playlist.attributes.RESOLUTION, 34804 codecs: playlist.attributes.CODECS 34805 }, 34806 cause: 'fast-quality' 34807 }; 34808 34809 if (enable !== currentlyEnabled && !incompatible) { 34810 // Ensure the outside world knows about our changes 34811 if (enable) { 34812 // call fast quality change only when the playlist is enabled 34813 changePlaylistFn(playlist); 34814 loader.trigger({ 34815 type: 'renditionenabled', 34816 metadata 34817 }); 34818 } else { 34819 loader.trigger({ 34820 type: 'renditiondisabled', 34821 metadata 34822 }); 34823 } 34824 } 34825 34826 return enable; 34827 }; 34828 /** 34829 * The representation object encapsulates the publicly visible information 34830 * in a media playlist along with a setter/getter-type function (enabled) 34831 * for changing the enabled-state of a particular playlist entry 34832 * 34833 * @class Representation 34834 */ 34835 34836 34837 class Representation { 34838 constructor(vhsHandler, playlist, id) { 34839 const { 34840 playlistController_: pc 34841 } = vhsHandler; 34842 const qualityChangeFunction = pc.fastQualityChange_.bind(pc); // some playlist attributes are optional 34843 34844 if (playlist.attributes) { 34845 const resolution = playlist.attributes.RESOLUTION; 34846 this.width = resolution && resolution.width; 34847 this.height = resolution && resolution.height; 34848 this.bandwidth = playlist.attributes.BANDWIDTH; 34849 this.frameRate = playlist.attributes['FRAME-RATE']; 34850 } 34851 34852 this.codecs = codecsForPlaylist(pc.main(), playlist); 34853 this.playlist = playlist; // The id is simply the ordinality of the media playlist 34854 // within the main playlist 34855 34856 this.id = id; // Partially-apply the enableFunction to create a playlist- 34857 // specific variant 34858 34859 this.enabled = enableFunction(vhsHandler.playlists, playlist.id, qualityChangeFunction); 34860 } 34861 34862 } 34863 /** 34864 * A mixin function that adds the `representations` api to an instance 34865 * of the VhsHandler class 34866 * 34867 * @param {VhsHandler} vhsHandler - An instance of VhsHandler to add the 34868 * representation API into 34869 */ 34870 34871 34872 const renditionSelectionMixin = function (vhsHandler) { 34873 // Add a single API-specific function to the VhsHandler instance 34874 vhsHandler.representations = () => { 34875 const main = vhsHandler.playlistController_.main(); 34876 const playlists = isAudioOnly(main) ? vhsHandler.playlistController_.getAudioTrackPlaylists_() : main.playlists; 34877 34878 if (!playlists) { 34879 return []; 34880 } 34881 34882 return playlists.filter(media => !isIncompatible(media)).map((e, i) => new Representation(vhsHandler, e, e.id)); 34883 }; 34884 }; 34885 34886 /** 34887 * @file playback-watcher.js 34888 * 34889 * Playback starts, and now my watch begins. It shall not end until my death. I shall 34890 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 34891 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 34892 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 34893 * my life and honor to the Playback Watch, for this Player and all the Players to come. 34894 */ 34895 34896 const timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 34897 /** 34898 * @class PlaybackWatcher 34899 */ 34900 34901 class PlaybackWatcher extends videojs__default["default"].EventTarget { 34902 /** 34903 * Represents an PlaybackWatcher object. 34904 * 34905 * @class 34906 * @param {Object} options an object that includes the tech and settings 34907 */ 34908 constructor(options) { 34909 super(); 34910 this.playlistController_ = options.playlistController; 34911 this.tech_ = options.tech; 34912 this.seekable = options.seekable; 34913 this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow; 34914 this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta; 34915 this.media = options.media; 34916 this.playedRanges_ = []; 34917 this.consecutiveUpdates = 0; 34918 this.lastRecordedTime = null; 34919 this.checkCurrentTimeTimeout_ = null; 34920 this.logger_ = logger('PlaybackWatcher'); 34921 this.logger_('initialize'); 34922 34923 const playHandler = () => this.monitorCurrentTime_(); 34924 34925 const canPlayHandler = () => this.monitorCurrentTime_(); 34926 34927 const waitingHandler = () => this.techWaiting_(); 34928 34929 const cancelTimerHandler = () => this.resetTimeUpdate_(); 34930 34931 const pc = this.playlistController_; 34932 const loaderTypes = ['main', 'subtitle', 'audio']; 34933 const loaderChecks = {}; 34934 loaderTypes.forEach(type => { 34935 loaderChecks[type] = { 34936 reset: () => this.resetSegmentDownloads_(type), 34937 updateend: () => this.checkSegmentDownloads_(type) 34938 }; 34939 pc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend); // If a rendition switch happens during a playback stall where the buffer 34940 // isn't changing we want to reset. We cannot assume that the new rendition 34941 // will also be stalled, until after new appends. 34942 34943 pc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset); // Playback stalls should not be detected right after seeking. 34944 // This prevents one segment playlists (single vtt or single segment content) 34945 // from being detected as stalling. As the buffer will not change in those cases, since 34946 // the buffer is the entire video duration. 34947 34948 this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset); 34949 }); 34950 /** 34951 * We check if a seek was into a gap through the following steps: 34952 * 1. We get a seeking event and we do not get a seeked event. This means that 34953 * a seek was attempted but not completed. 34954 * 2. We run `fixesBadSeeks_` on segment loader appends. This means that we already 34955 * removed everything from our buffer and appended a segment, and should be ready 34956 * to check for gaps. 34957 */ 34958 34959 const setSeekingHandlers = fn => { 34960 ['main', 'audio'].forEach(type => { 34961 pc[`${type}SegmentLoader_`][fn]('appended', this.seekingAppendCheck_); 34962 }); 34963 }; 34964 34965 this.seekingAppendCheck_ = () => { 34966 if (this.fixesBadSeeks_()) { 34967 this.consecutiveUpdates = 0; 34968 this.lastRecordedTime = this.tech_.currentTime(); 34969 setSeekingHandlers('off'); 34970 } 34971 }; 34972 34973 this.clearSeekingAppendCheck_ = () => setSeekingHandlers('off'); 34974 34975 this.watchForBadSeeking_ = () => { 34976 this.clearSeekingAppendCheck_(); 34977 setSeekingHandlers('on'); 34978 }; 34979 34980 this.tech_.on('seeked', this.clearSeekingAppendCheck_); 34981 this.tech_.on('seeking', this.watchForBadSeeking_); 34982 this.tech_.on('waiting', waitingHandler); 34983 this.tech_.on(timerCancelEvents, cancelTimerHandler); 34984 this.tech_.on('canplay', canPlayHandler); 34985 /* 34986 An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case 34987 is surfaced in one of two ways: 34988 1) The `waiting` event is fired before the player has buffered content, making it impossible 34989 to find or skip the gap. The `waiting` event is followed by a `play` event. On first play 34990 we can check if playback is stalled due to a gap, and skip the gap if necessary. 34991 2) A source with a gap at the beginning of the stream is loaded programatically while the player 34992 is in a playing state. To catch this case, it's important that our one-time play listener is setup 34993 even if the player is in a playing state 34994 */ 34995 34996 this.tech_.one('play', playHandler); // Define the dispose function to clean up our events 34997 34998 this.dispose = () => { 34999 this.clearSeekingAppendCheck_(); 35000 this.logger_('dispose'); 35001 this.tech_.off('waiting', waitingHandler); 35002 this.tech_.off(timerCancelEvents, cancelTimerHandler); 35003 this.tech_.off('canplay', canPlayHandler); 35004 this.tech_.off('play', playHandler); 35005 this.tech_.off('seeking', this.watchForBadSeeking_); 35006 this.tech_.off('seeked', this.clearSeekingAppendCheck_); 35007 loaderTypes.forEach(type => { 35008 pc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend); 35009 pc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset); 35010 this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset); 35011 }); 35012 35013 if (this.checkCurrentTimeTimeout_) { 35014 window.clearTimeout(this.checkCurrentTimeTimeout_); 35015 } 35016 35017 this.resetTimeUpdate_(); 35018 }; 35019 } 35020 /** 35021 * Periodically check current time to see if playback stopped 35022 * 35023 * @private 35024 */ 35025 35026 35027 monitorCurrentTime_() { 35028 this.checkCurrentTime_(); 35029 35030 if (this.checkCurrentTimeTimeout_) { 35031 window.clearTimeout(this.checkCurrentTimeTimeout_); 35032 } // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 35033 35034 35035 this.checkCurrentTimeTimeout_ = window.setTimeout(this.monitorCurrentTime_.bind(this), 250); 35036 } 35037 /** 35038 * Reset stalled download stats for a specific type of loader 35039 * 35040 * @param {string} type 35041 * The segment loader type to check. 35042 * 35043 * @listens SegmentLoader#playlistupdate 35044 * @listens Tech#seeking 35045 * @listens Tech#seeked 35046 */ 35047 35048 35049 resetSegmentDownloads_(type) { 35050 const loader = this.playlistController_[`${type}SegmentLoader_`]; 35051 35052 if (this[`${type}StalledDownloads_`] > 0) { 35053 this.logger_(`resetting possible stalled download count for ${type} loader`); 35054 } 35055 35056 this[`${type}StalledDownloads_`] = 0; 35057 this[`${type}Buffered_`] = loader.buffered_(); 35058 } 35059 /** 35060 * Checks on every segment `appendsdone` to see 35061 * if segment appends are making progress. If they are not 35062 * and we are still downloading bytes. We exclude the playlist. 35063 * 35064 * @param {string} type 35065 * The segment loader type to check. 35066 * 35067 * @listens SegmentLoader#appendsdone 35068 */ 35069 35070 35071 checkSegmentDownloads_(type) { 35072 const pc = this.playlistController_; 35073 const loader = pc[`${type}SegmentLoader_`]; 35074 const buffered = loader.buffered_(); 35075 const isBufferedDifferent = isRangeDifferent(this[`${type}Buffered_`], buffered); 35076 this[`${type}Buffered_`] = buffered; // if another watcher is going to fix the issue or 35077 // the buffered value for this loader changed 35078 // appends are working 35079 35080 if (isBufferedDifferent) { 35081 const metadata = { 35082 bufferedRanges: buffered 35083 }; 35084 pc.trigger({ 35085 type: 'bufferedrangeschanged', 35086 metadata 35087 }); 35088 this.resetSegmentDownloads_(type); 35089 return; 35090 } 35091 35092 this[`${type}StalledDownloads_`]++; 35093 this.logger_(`found #${this[`${type}StalledDownloads_`]} ${type} appends that did not increase buffer (possible stalled download)`, { 35094 playlistId: loader.playlist_ && loader.playlist_.id, 35095 buffered: timeRangesToArray(buffered) 35096 }); // after 10 possibly stalled appends with no reset, exclude 35097 35098 if (this[`${type}StalledDownloads_`] < 10) { 35099 return; 35100 } 35101 35102 this.logger_(`${type} loader stalled download exclusion`); 35103 this.resetSegmentDownloads_(type); 35104 this.tech_.trigger({ 35105 type: 'usage', 35106 name: `vhs-${type}-download-exclusion` 35107 }); 35108 35109 if (type === 'subtitle') { 35110 return; 35111 } // TODO: should we exclude audio tracks rather than main tracks 35112 // when type is audio? 35113 35114 35115 pc.excludePlaylist({ 35116 error: { 35117 message: `Excessive ${type} segment downloading detected.` 35118 }, 35119 playlistExclusionDuration: Infinity 35120 }); 35121 } 35122 /** 35123 * The purpose of this function is to emulate the "waiting" event on 35124 * browsers that do not emit it when they are waiting for more 35125 * data to continue playback 35126 * 35127 * @private 35128 */ 35129 35130 35131 checkCurrentTime_() { 35132 if (this.tech_.paused() || this.tech_.seeking()) { 35133 return; 35134 } 35135 35136 const currentTime = this.tech_.currentTime(); 35137 const buffered = this.tech_.buffered(); 35138 35139 if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 35140 // If current time is at the end of the final buffered region, then any playback 35141 // stall is most likely caused by buffering in a low bandwidth environment. The tech 35142 // should fire a `waiting` event in this scenario, but due to browser and tech 35143 // inconsistencies. Calling `techWaiting_` here allows us to simulate 35144 // responding to a native `waiting` event when the tech fails to emit one. 35145 return this.techWaiting_(); 35146 } 35147 35148 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 35149 this.consecutiveUpdates++; 35150 this.waiting_(); 35151 } else if (currentTime === this.lastRecordedTime) { 35152 this.consecutiveUpdates++; 35153 } else { 35154 this.playedRanges_.push(createTimeRanges([this.lastRecordedTime, currentTime])); 35155 const metadata = { 35156 playedRanges: this.playedRanges_ 35157 }; 35158 this.playlistController_.trigger({ 35159 type: 'playedrangeschanged', 35160 metadata 35161 }); 35162 this.consecutiveUpdates = 0; 35163 this.lastRecordedTime = currentTime; 35164 } 35165 } 35166 /** 35167 * Resets the 'timeupdate' mechanism designed to detect that we are stalled 35168 * 35169 * @private 35170 */ 35171 35172 35173 resetTimeUpdate_() { 35174 this.consecutiveUpdates = 0; 35175 } 35176 /** 35177 * Fixes situations where there's a bad seek 35178 * 35179 * @return {boolean} whether an action was taken to fix the seek 35180 * @private 35181 */ 35182 35183 35184 fixesBadSeeks_() { 35185 const seeking = this.tech_.seeking(); 35186 35187 if (!seeking) { 35188 return false; 35189 } // TODO: It's possible that these seekable checks should be moved out of this function 35190 // and into a function that runs on seekablechange. It's also possible that we only need 35191 // afterSeekableWindow as the buffered check at the bottom is good enough to handle before 35192 // seekable range. 35193 35194 35195 const seekable = this.seekable(); 35196 const currentTime = this.tech_.currentTime(); 35197 const isAfterSeekableRange = this.afterSeekableWindow_(seekable, currentTime, this.media(), this.allowSeeksWithinUnsafeLiveWindow); 35198 let seekTo; 35199 35200 if (isAfterSeekableRange) { 35201 const seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 35202 35203 seekTo = seekableEnd; 35204 } 35205 35206 if (this.beforeSeekableWindow_(seekable, currentTime)) { 35207 const seekableStart = seekable.start(0); // sync to the beginning of the live window 35208 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 35209 35210 seekTo = seekableStart + ( // if the playlist is too short and the seekable range is an exact time (can 35211 // happen in live with a 3 segment playlist), then don't use a time delta 35212 seekableStart === seekable.end(0) ? 0 : SAFE_TIME_DELTA); 35213 } 35214 35215 if (typeof seekTo !== 'undefined') { 35216 this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` + `seekable range ${printableRange(seekable)}. Seeking to ` + `${seekTo}.`); 35217 this.tech_.setCurrentTime(seekTo); 35218 return true; 35219 } 35220 35221 const sourceUpdater = this.playlistController_.sourceUpdater_; 35222 const buffered = this.tech_.buffered(); 35223 const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null; 35224 const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null; 35225 const media = this.media(); // verify that at least two segment durations or one part duration have been 35226 // appended before checking for a gap. 35227 35228 const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration : (media.targetDuration - TIME_FUDGE_FACTOR) * 2; // verify that at least two segment durations have been 35229 // appended before checking for a gap. 35230 35231 const bufferedToCheck = [audioBuffered, videoBuffered]; 35232 35233 for (let i = 0; i < bufferedToCheck.length; i++) { 35234 // skip null buffered 35235 if (!bufferedToCheck[i]) { 35236 continue; 35237 } 35238 35239 const timeAhead = timeAheadOf(bufferedToCheck[i], currentTime); // if we are less than two video/audio segment durations or one part 35240 // duration behind we haven't appended enough to call this a bad seek. 35241 35242 if (timeAhead < minAppendedDuration) { 35243 return false; 35244 } 35245 } 35246 35247 const nextRange = findNextRange(buffered, currentTime); // we have appended enough content, but we don't have anything buffered 35248 // to seek over the gap 35249 35250 if (nextRange.length === 0) { 35251 return false; 35252 } 35253 35254 seekTo = nextRange.start(0) + SAFE_TIME_DELTA; 35255 this.logger_(`Buffered region starts (${nextRange.start(0)}) ` + ` just beyond seek point (${currentTime}). Seeking to ${seekTo}.`); 35256 this.tech_.setCurrentTime(seekTo); 35257 return true; 35258 } 35259 /** 35260 * Handler for situations when we determine the player is waiting. 35261 * 35262 * @private 35263 */ 35264 35265 35266 waiting_() { 35267 if (this.techWaiting_()) { 35268 return; 35269 } // All tech waiting checks failed. Use last resort correction 35270 35271 35272 const currentTime = this.tech_.currentTime(); 35273 const buffered = this.tech_.buffered(); 35274 const currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered 35275 // region with no indication that anything is amiss (seen in Firefox). Seeking to 35276 // currentTime is usually enough to kickstart the player. This checks that the player 35277 // is currently within a buffered region before attempting a corrective seek. 35278 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 35279 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 35280 // make sure there is ~3 seconds of forward buffer before taking any corrective action 35281 // to avoid triggering an `unknownwaiting` event when the network is slow. 35282 35283 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 35284 this.resetTimeUpdate_(); 35285 this.tech_.setCurrentTime(currentTime); 35286 this.logger_(`Stopped at ${currentTime} while inside a buffered region ` + `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` + 'playback by seeking to the current time.'); // unknown waiting corrections may be useful for monitoring QoS 35287 35288 this.tech_.trigger({ 35289 type: 'usage', 35290 name: 'vhs-unknown-waiting' 35291 }); 35292 return; 35293 } 35294 } 35295 /** 35296 * Handler for situations when the tech fires a `waiting` event 35297 * 35298 * @return {boolean} 35299 * True if an action (or none) was needed to correct the waiting. False if no 35300 * checks passed 35301 * @private 35302 */ 35303 35304 35305 techWaiting_() { 35306 const seekable = this.seekable(); 35307 const currentTime = this.tech_.currentTime(); 35308 35309 if (this.tech_.seeking()) { 35310 // Tech is seeking or already waiting on another action, no action needed 35311 return true; 35312 } 35313 35314 if (this.beforeSeekableWindow_(seekable, currentTime)) { 35315 const livePoint = seekable.end(seekable.length - 1); 35316 this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` + `live point (seekable end) ${livePoint}`); 35317 this.resetTimeUpdate_(); 35318 this.tech_.setCurrentTime(livePoint); // live window resyncs may be useful for monitoring QoS 35319 35320 this.tech_.trigger({ 35321 type: 'usage', 35322 name: 'vhs-live-resync' 35323 }); 35324 return true; 35325 } 35326 35327 const sourceUpdater = this.tech_.vhs.playlistController_.sourceUpdater_; 35328 const buffered = this.tech_.buffered(); 35329 const videoUnderflow = this.videoUnderflow_({ 35330 audioBuffered: sourceUpdater.audioBuffered(), 35331 videoBuffered: sourceUpdater.videoBuffered(), 35332 currentTime 35333 }); 35334 35335 if (videoUnderflow) { 35336 // Even though the video underflowed and was stuck in a gap, the audio overplayed 35337 // the gap, leading currentTime into a buffered range. Seeking to currentTime 35338 // allows the video to catch up to the audio position without losing any audio 35339 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 35340 this.resetTimeUpdate_(); 35341 this.tech_.setCurrentTime(currentTime); // video underflow may be useful for monitoring QoS 35342 35343 this.tech_.trigger({ 35344 type: 'usage', 35345 name: 'vhs-video-underflow' 35346 }); 35347 return true; 35348 } 35349 35350 const nextRange = findNextRange(buffered, currentTime); // check for gap 35351 35352 if (nextRange.length > 0) { 35353 this.logger_(`Stopped at ${currentTime} and seeking to ${nextRange.start(0)}`); 35354 this.resetTimeUpdate_(); 35355 this.skipTheGap_(currentTime); 35356 return true; 35357 } // All checks failed. Returning false to indicate failure to correct waiting 35358 35359 35360 return false; 35361 } 35362 35363 afterSeekableWindow_(seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) { 35364 if (!seekable.length) { 35365 // we can't make a solid case if there's no seekable, default to false 35366 return false; 35367 } 35368 35369 let allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA; 35370 const isLive = !playlist.endList; 35371 const isLLHLS = typeof playlist.partTargetDuration === 'number'; 35372 35373 if (isLive && (isLLHLS || allowSeeksWithinUnsafeLiveWindow)) { 35374 allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3; 35375 } 35376 35377 if (currentTime > allowedEnd) { 35378 return true; 35379 } 35380 35381 return false; 35382 } 35383 35384 beforeSeekableWindow_(seekable, currentTime) { 35385 if (seekable.length && // can't fall before 0 and 0 seekable start identifies VOD stream 35386 seekable.start(0) > 0 && currentTime < seekable.start(0) - this.liveRangeSafeTimeDelta) { 35387 return true; 35388 } 35389 35390 return false; 35391 } 35392 35393 videoUnderflow_({ 35394 videoBuffered, 35395 audioBuffered, 35396 currentTime 35397 }) { 35398 // audio only content will not have video underflow :) 35399 if (!videoBuffered) { 35400 return; 35401 } 35402 35403 let gap; // find a gap in demuxed content. 35404 35405 if (videoBuffered.length && audioBuffered.length) { 35406 // in Chrome audio will continue to play for ~3s when we run out of video 35407 // so we have to check that the video buffer did have some buffer in the 35408 // past. 35409 const lastVideoRange = findRange(videoBuffered, currentTime - 3); 35410 const videoRange = findRange(videoBuffered, currentTime); 35411 const audioRange = findRange(audioBuffered, currentTime); 35412 35413 if (audioRange.length && !videoRange.length && lastVideoRange.length) { 35414 gap = { 35415 start: lastVideoRange.end(0), 35416 end: audioRange.end(0) 35417 }; 35418 } // find a gap in muxed content. 35419 35420 } else { 35421 const nextRange = findNextRange(videoBuffered, currentTime); // Even if there is no available next range, there is still a possibility we are 35422 // stuck in a gap due to video underflow. 35423 35424 if (!nextRange.length) { 35425 gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime); 35426 } 35427 } 35428 35429 if (gap) { 35430 this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` + `Seeking to current time ${currentTime}`); 35431 return true; 35432 } 35433 35434 return false; 35435 } 35436 /** 35437 * Timer callback. If playback still has not proceeded, then we seek 35438 * to the start of the next buffered region. 35439 * 35440 * @private 35441 */ 35442 35443 35444 skipTheGap_(scheduledCurrentTime) { 35445 const buffered = this.tech_.buffered(); 35446 const currentTime = this.tech_.currentTime(); 35447 const nextRange = findNextRange(buffered, currentTime); 35448 this.resetTimeUpdate_(); 35449 35450 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 35451 return; 35452 } 35453 35454 this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played 35455 35456 this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR); 35457 const metadata = { 35458 gapInfo: { 35459 from: currentTime, 35460 to: nextRange.start(0) 35461 } 35462 }; 35463 this.playlistController_.trigger({ 35464 type: 'gapjumped', 35465 metadata 35466 }); 35467 this.tech_.trigger({ 35468 type: 'usage', 35469 name: 'vhs-gap-skip' 35470 }); 35471 } 35472 35473 gapFromVideoUnderflow_(buffered, currentTime) { 35474 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 35475 // playing for ~3 seconds after the video gap starts. This is done to account for 35476 // video buffer underflow/underrun (note that this is not done when there is audio 35477 // buffer underflow/underrun -- in that case the video will stop as soon as it 35478 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 35479 // video stalls). The player's time will reflect the playthrough of audio, so the 35480 // time will appear as if we are in a buffered region, even if we are stuck in a 35481 // "gap." 35482 // 35483 // Example: 35484 // video buffer: 0 => 10.1, 10.2 => 20 35485 // audio buffer: 0 => 20 35486 // overall buffer: 0 => 10.1, 10.2 => 20 35487 // current time: 13 35488 // 35489 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 35490 // however, the audio continued playing until it reached ~3 seconds past the gap 35491 // (13 seconds), at which point it stops as well. Since current time is past the 35492 // gap, findNextRange will return no ranges. 35493 // 35494 // To check for this issue, we see if there is a gap that starts somewhere within 35495 // a 3 second range (3 seconds +/- 1 second) back from our current time. 35496 const gaps = findGaps(buffered); 35497 35498 for (let i = 0; i < gaps.length; i++) { 35499 const start = gaps.start(i); 35500 const end = gaps.end(i); // gap is starts no more than 4 seconds back 35501 35502 if (currentTime - start < 4 && currentTime - start > 2) { 35503 return { 35504 start, 35505 end 35506 }; 35507 } 35508 } 35509 35510 return null; 35511 } 35512 35513 } 35514 35515 const defaultOptions = { 35516 errorInterval: 30, 35517 35518 getSource(next) { 35519 const tech = this.tech({ 35520 IWillNotUseThisInPlugins: true 35521 }); 35522 const sourceObj = tech.currentSource_ || this.currentSource(); 35523 return next(sourceObj); 35524 } 35525 35526 }; 35527 /** 35528 * Main entry point for the plugin 35529 * 35530 * @param {Player} player a reference to a videojs Player instance 35531 * @param {Object} [options] an object with plugin options 35532 * @private 35533 */ 35534 35535 const initPlugin = function (player, options) { 35536 let lastCalled = 0; 35537 let seekTo = 0; 35538 const localOptions = merge$1(defaultOptions, options); 35539 player.ready(() => { 35540 player.trigger({ 35541 type: 'usage', 35542 name: 'vhs-error-reload-initialized' 35543 }); 35544 }); 35545 /** 35546 * Player modifications to perform that must wait until `loadedmetadata` 35547 * has been triggered 35548 * 35549 * @private 35550 */ 35551 35552 const loadedMetadataHandler = function () { 35553 if (seekTo) { 35554 player.currentTime(seekTo); 35555 } 35556 }; 35557 /** 35558 * Set the source on the player element, play, and seek if necessary 35559 * 35560 * @param {Object} sourceObj An object specifying the source url and mime-type to play 35561 * @private 35562 */ 35563 35564 35565 const setSource = function (sourceObj) { 35566 if (sourceObj === null || sourceObj === undefined) { 35567 return; 35568 } 35569 35570 seekTo = player.duration() !== Infinity && player.currentTime() || 0; 35571 player.one('loadedmetadata', loadedMetadataHandler); 35572 player.src(sourceObj); 35573 player.trigger({ 35574 type: 'usage', 35575 name: 'vhs-error-reload' 35576 }); 35577 player.play(); 35578 }; 35579 /** 35580 * Attempt to get a source from either the built-in getSource function 35581 * or a custom function provided via the options 35582 * 35583 * @private 35584 */ 35585 35586 35587 const errorHandler = function () { 35588 // Do not attempt to reload the source if a source-reload occurred before 35589 // 'errorInterval' time has elapsed since the last source-reload 35590 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 35591 player.trigger({ 35592 type: 'usage', 35593 name: 'vhs-error-reload-canceled' 35594 }); 35595 return; 35596 } 35597 35598 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 35599 videojs__default["default"].log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 35600 return; 35601 } 35602 35603 lastCalled = Date.now(); 35604 return localOptions.getSource.call(player, setSource); 35605 }; 35606 /** 35607 * Unbind any event handlers that were bound by the plugin 35608 * 35609 * @private 35610 */ 35611 35612 35613 const cleanupEvents = function () { 35614 player.off('loadedmetadata', loadedMetadataHandler); 35615 player.off('error', errorHandler); 35616 player.off('dispose', cleanupEvents); 35617 }; 35618 /** 35619 * Cleanup before re-initializing the plugin 35620 * 35621 * @param {Object} [newOptions] an object with plugin options 35622 * @private 35623 */ 35624 35625 35626 const reinitPlugin = function (newOptions) { 35627 cleanupEvents(); 35628 initPlugin(player, newOptions); 35629 }; 35630 35631 player.on('error', errorHandler); 35632 player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before 35633 // initializing the plugin 35634 35635 player.reloadSourceOnError = reinitPlugin; 35636 }; 35637 /** 35638 * Reload the source when an error is detected as long as there 35639 * wasn't an error previously within the last 30 seconds 35640 * 35641 * @param {Object} [options] an object with plugin options 35642 */ 35643 35644 35645 const reloadSourceOnError = function (options) { 35646 initPlugin(this, options); 35647 }; 35648 35649 var version$4 = "3.17.
356492"; 35650 35651 var version$3 = "7.1.0"; 35652 35653 var version$2 = "1.3.1
vendor: 47,376 bytes, lines 35653-37013
35653"; 35654 35655 var version$1 = "7.2.0"; 35656 35657 var version = "4.0.2"; 35658 35659 const Vhs = { 35660 PlaylistLoader, 35661 Playlist, 35662 utils, 35663 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 35664 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 35665 lastBandwidthSelector, 35666 movingAverageBandwidthSelector, 35667 comparePlaylistBandwidth, 35668 comparePlaylistResolution, 35669 xhr: xhrFactory() 35670 }; // Define getter/setters for config properties 35671 35672 Object.keys(Config).forEach(prop => { 35673 Object.defineProperty(Vhs, prop, { 35674 get() { 35675 videojs__default["default"].log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 35676 return Config[prop]; 35677 }, 35678 35679 set(value) { 35680 videojs__default["default"].log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 35681 35682 if (typeof value !== 'number' || value < 0) { 35683 videojs__default["default"].log.warn(`value of Vhs.${prop} must be greater than or equal to 0`); 35684 return; 35685 } 35686 35687 Config[prop] = value; 35688 } 35689 35690 }); 35691 }); 35692 const LOCAL_STORAGE_KEY = 'videojs-vhs'; 35693 /** 35694 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs. 35695 * 35696 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 35697 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 35698 * @function handleVhsMediaChange 35699 */ 35700 35701 const handleVhsMediaChange = function (qualityLevels, playlistLoader) { 35702 const newPlaylist = playlistLoader.media(); 35703 let selectedIndex = -1; 35704 35705 for (let i = 0; i < qualityLevels.length; i++) { 35706 if (qualityLevels[i].id === newPlaylist.id) { 35707 selectedIndex = i; 35708 break; 35709 } 35710 } 35711 35712 qualityLevels.selectedIndex_ = selectedIndex; 35713 qualityLevels.trigger({ 35714 selectedIndex, 35715 type: 'change' 35716 }); 35717 }; 35718 /** 35719 * Adds quality levels to list once playlist metadata is available 35720 * 35721 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 35722 * @param {Object} vhs Vhs object to listen to for media events. 35723 * @function handleVhsLoadedMetadata 35724 */ 35725 35726 35727 const handleVhsLoadedMetadata = function (qualityLevels, vhs) { 35728 vhs.representations().forEach(rep => { 35729 qualityLevels.addQualityLevel(rep); 35730 }); 35731 handleVhsMediaChange(qualityLevels, vhs.playlists); 35732 }; // VHS is a source handler, not a tech. Make sure attempts to use it 35733 // as one do not cause exceptions. 35734 35735 35736 Vhs.canPlaySource = function () { 35737 return videojs__default["default"].log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 35738 }; 35739 35740 const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => { 35741 if (!keySystemOptions) { 35742 return keySystemOptions; 35743 } 35744 35745 let codecs = {}; 35746 35747 if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) { 35748 codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS)); 35749 } 35750 35751 if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) { 35752 codecs.audio = audioPlaylist.attributes.CODECS; 35753 } 35754 35755 const videoContentType = getMimeForCodec(codecs.video); 35756 const audioContentType = getMimeForCodec(codecs.audio); // upsert the content types based on the selected playlist 35757 35758 const keySystemContentTypes = {}; 35759 35760 for (const keySystem in keySystemOptions) { 35761 keySystemContentTypes[keySystem] = {}; 35762 35763 if (audioContentType) { 35764 keySystemContentTypes[keySystem].audioContentType = audioContentType; 35765 } 35766 35767 if (videoContentType) { 35768 keySystemContentTypes[keySystem].videoContentType = videoContentType; 35769 } // Default to using the video playlist's PSSH even though they may be different, as 35770 // videojs-contrib-eme will only accept one in the options. 35771 // 35772 // This shouldn't be an issue for most cases as early intialization will handle all 35773 // unique PSSH values, and if they aren't, then encrypted events should have the 35774 // specific information needed for the unique license. 35775 35776 35777 if (mainPlaylist.contentProtection && mainPlaylist.contentProtection[keySystem] && mainPlaylist.contentProtection[keySystem].pssh) { 35778 keySystemContentTypes[keySystem].pssh = mainPlaylist.contentProtection[keySystem].pssh; 35779 } // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url' 35780 // so we need to prevent overwriting the URL entirely 35781 35782 35783 if (typeof keySystemOptions[keySystem] === 'string') { 35784 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 35785 } 35786 } 35787 35788 return merge$1(keySystemOptions, keySystemContentTypes); 35789 }; 35790 /** 35791 * @typedef {Object} KeySystems 35792 * 35793 * keySystems configuration for https://github.com/videojs/videojs-contrib-eme 35794 * Note: not all options are listed here. 35795 * 35796 * @property {Uint8Array} [pssh] 35797 * Protection System Specific Header 35798 */ 35799 35800 /** 35801 * Goes through all the playlists and collects an array of KeySystems options objects 35802 * containing each playlist's keySystems and their pssh values, if available. 35803 * 35804 * @param {Object[]} playlists 35805 * The playlists to look through 35806 * @param {string[]} keySystems 35807 * The keySystems to collect pssh values for 35808 * 35809 * @return {KeySystems[]} 35810 * An array of KeySystems objects containing available key systems and their 35811 * pssh values 35812 */ 35813 35814 35815 const getAllPsshKeySystemsOptions = (playlists, keySystems) => { 35816 return playlists.reduce((keySystemsArr, playlist) => { 35817 if (!playlist.contentProtection) { 35818 return keySystemsArr; 35819 } 35820 35821 const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => { 35822 const keySystemOptions = playlist.contentProtection[keySystem]; 35823 35824 if (keySystemOptions && keySystemOptions.pssh) { 35825 keySystemsObj[keySystem] = { 35826 pssh: keySystemOptions.pssh 35827 }; 35828 } 35829 35830 return keySystemsObj; 35831 }, {}); 35832 35833 if (Object.keys(keySystemsOptions).length) { 35834 keySystemsArr.push(keySystemsOptions); 35835 } 35836 35837 return keySystemsArr; 35838 }, []); 35839 }; 35840 /** 35841 * Returns a promise that waits for the 35842 * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session. 35843 * 35844 * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11 35845 * browsers. 35846 * 35847 * As per the above ticket, this is particularly important for Chrome, where, if 35848 * unencrypted content is appended before encrypted content and the key session has not 35849 * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached 35850 * during playback. 35851 * 35852 * @param {Object} player 35853 * The player instance 35854 * @param {Object[]} sourceKeySystems 35855 * The key systems options from the player source 35856 * @param {Object} [audioMedia] 35857 * The active audio media playlist (optional) 35858 * @param {Object[]} mainPlaylists 35859 * The playlists found on the main playlist object 35860 * 35861 * @return {Object} 35862 * Promise that resolves when the key session has been created 35863 */ 35864 35865 35866 const waitForKeySessionCreation = ({ 35867 player, 35868 sourceKeySystems, 35869 audioMedia, 35870 mainPlaylists 35871 }) => { 35872 if (!player.eme.initializeMediaKeys) { 35873 return Promise.resolve(); 35874 } // TODO should all audio PSSH values be initialized for DRM? 35875 // 35876 // All unique video rendition pssh values are initialized for DRM, but here only 35877 // the initial audio playlist license is initialized. In theory, an encrypted 35878 // event should be fired if the user switches to an alternative audio playlist 35879 // where a license is required, but this case hasn't yet been tested. In addition, there 35880 // may be many alternate audio playlists unlikely to be used (e.g., multiple different 35881 // languages). 35882 35883 35884 const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists; 35885 const keySystemsOptionsArr = getAllPsshKeySystemsOptions(playlists, Object.keys(sourceKeySystems)); 35886 const initializationFinishedPromises = []; 35887 const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The 35888 // only place where it should not be deduped is for ms-prefixed APIs, but 35889 // the existence of modern EME APIs in addition to 35890 // ms-prefixed APIs on Edge should prevent this from being a concern. 35891 // initializeMediaKeys also won't use the webkit-prefixed APIs. 35892 35893 keySystemsOptionsArr.forEach(keySystemsOptions => { 35894 keySessionCreatedPromises.push(new Promise((resolve, reject) => { 35895 player.tech_.one('keysessioncreated', resolve); 35896 })); 35897 initializationFinishedPromises.push(new Promise((resolve, reject) => { 35898 player.eme.initializeMediaKeys({ 35899 keySystems: keySystemsOptions 35900 }, err => { 35901 if (err) { 35902 reject(err); 35903 return; 35904 } 35905 35906 resolve(); 35907 }); 35908 })); 35909 }); // The reasons Promise.race is chosen over Promise.any: 35910 // 35911 // * Promise.any is only available in Safari 14+. 35912 // * None of these promises are expected to reject. If they do reject, it might be 35913 // better here for the race to surface the rejection, rather than mask it by using 35914 // Promise.any. 35915 35916 return Promise.race([// If a session was previously created, these will all finish resolving without 35917 // creating a new session, otherwise it will take until the end of all license 35918 // requests, which is why the key session check is used (to make setup much faster). 35919 Promise.all(initializationFinishedPromises), // Once a single session is created, the browser knows DRM will be used. 35920 Promise.race(keySessionCreatedPromises)]); 35921 }; 35922 /** 35923 * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and 35924 * there are keySystems on the source, sets up source options to prepare the source for 35925 * eme. 35926 * 35927 * @param {Object} player 35928 * The player instance 35929 * @param {Object[]} sourceKeySystems 35930 * The key systems options from the player source 35931 * @param {Object} media 35932 * The active media playlist 35933 * @param {Object} [audioMedia] 35934 * The active audio media playlist (optional) 35935 * 35936 * @return {boolean} 35937 * Whether or not options were configured and EME is available 35938 */ 35939 35940 const setupEmeOptions = ({ 35941 player, 35942 sourceKeySystems, 35943 media, 35944 audioMedia 35945 }) => { 35946 const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia); 35947 35948 if (!sourceOptions) { 35949 return false; 35950 } 35951 35952 player.currentSource().keySystems = sourceOptions; // eme handles the rest of the setup, so if it is missing 35953 // do nothing. 35954 35955 if (sourceOptions && !player.eme) { 35956 videojs__default["default"].log.warn('DRM encrypted source cannot be decrypted without a DRM plugin'); 35957 return false; 35958 } 35959 35960 return true; 35961 }; 35962 35963 const getVhsLocalStorage = () => { 35964 if (!window.localStorage) { 35965 return null; 35966 } 35967 35968 const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY); 35969 35970 if (!storedObject) { 35971 return null; 35972 } 35973 35974 try { 35975 return JSON.parse(storedObject); 35976 } catch (e) { 35977 // someone may have tampered with the value 35978 return null; 35979 } 35980 }; 35981 35982 const updateVhsLocalStorage = options => { 35983 if (!window.localStorage) { 35984 return false; 35985 } 35986 35987 let objectToStore = getVhsLocalStorage(); 35988 objectToStore = objectToStore ? merge$1(objectToStore, options) : options; 35989 35990 try { 35991 window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore)); 35992 } catch (e) { 35993 // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where 35994 // storage is set to 0). 35995 // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions 35996 // No need to perform any operation. 35997 return false; 35998 } 35999 36000 return objectToStore; 36001 }; 36002 /** 36003 * Parses VHS-supported media types from data URIs. See 36004 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 36005 * for information on data URIs. 36006 * 36007 * @param {string} dataUri 36008 * The data URI 36009 * 36010 * @return {string|Object} 36011 * The parsed object/string, or the original string if no supported media type 36012 * was found 36013 */ 36014 36015 36016 const expandDataUri = dataUri => { 36017 if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) { 36018 return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1)); 36019 } // no known case for this data URI, return the string as-is 36020 36021 36022 return dataUri; 36023 }; 36024 /** 36025 * Adds a request hook to an xhr object 36026 * 36027 * @param {Object} xhr object to add the onRequest hook to 36028 * @param {function} callback hook function for an xhr request 36029 */ 36030 36031 36032 const addOnRequestHook = (xhr, callback) => { 36033 if (!xhr._requestCallbackSet) { 36034 xhr._requestCallbackSet = new Set(); 36035 } 36036 36037 xhr._requestCallbackSet.add(callback); 36038 }; 36039 /** 36040 * Adds a response hook to an xhr object 36041 * 36042 * @param {Object} xhr object to add the onResponse hook to 36043 * @param {function} callback hook function for an xhr response 36044 */ 36045 36046 36047 const addOnResponseHook = (xhr, callback) => { 36048 if (!xhr._responseCallbackSet) { 36049 xhr._responseCallbackSet = new Set(); 36050 } 36051 36052 xhr._responseCallbackSet.add(callback); 36053 }; 36054 /** 36055 * Removes a request hook on an xhr object, deletes the onRequest set if empty. 36056 * 36057 * @param {Object} xhr object to remove the onRequest hook from 36058 * @param {function} callback hook function to remove 36059 */ 36060 36061 36062 const removeOnRequestHook = (xhr, callback) => { 36063 if (!xhr._requestCallbackSet) { 36064 return; 36065 } 36066 36067 xhr._requestCallbackSet.delete(callback); 36068 36069 if (!xhr._requestCallbackSet.size) { 36070 delete xhr._requestCallbackSet; 36071 } 36072 }; 36073 /** 36074 * Removes a response hook on an xhr object, deletes the onResponse set if empty. 36075 * 36076 * @param {Object} xhr object to remove the onResponse hook from 36077 * @param {function} callback hook function to remove 36078 */ 36079 36080 36081 const removeOnResponseHook = (xhr, callback) => { 36082 if (!xhr._responseCallbackSet) { 36083 return; 36084 } 36085 36086 xhr._responseCallbackSet.delete(callback); 36087 36088 if (!xhr._responseCallbackSet.size) { 36089 delete xhr._responseCallbackSet; 36090 } 36091 }; 36092 /** 36093 * Whether the browser has built-in HLS support. 36094 */ 36095 36096 36097 Vhs.supportsNativeHls = function () { 36098 if (!document || !document.createElement) { 36099 return false; 36100 } 36101 36102 const video = document.createElement('video'); // native HLS is definitely not supported if HTML5 video isn't 36103 36104 if (!videojs__default["default"].getTech('Html5').isSupported()) { 36105 return false; 36106 } // HLS manifests can go by many mime-types 36107 36108 36109 const canPlay = [// Apple santioned 36110 'application/vnd.apple.mpegurl', // Apple sanctioned for backwards compatibility 36111 'audio/mpegurl', // Very common 36112 'audio/x-mpegurl', // Very common 36113 'application/x-mpegurl', // Included for completeness 36114 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; 36115 return canPlay.some(function (canItPlay) { 36116 return /maybe|probably/i.test(video.canPlayType(canItPlay)); 36117 }); 36118 }(); 36119 36120 Vhs.supportsNativeDash = function () { 36121 if (!document || !document.createElement || !videojs__default["default"].getTech('Html5').isSupported()) { 36122 return false; 36123 } 36124 36125 return /maybe|probably/i.test(document.createElement('video').canPlayType('application/dash+xml')); 36126 }(); 36127 36128 Vhs.supportsTypeNatively = type => { 36129 if (type === 'hls') { 36130 return Vhs.supportsNativeHls; 36131 } 36132 36133 if (type === 'dash') { 36134 return Vhs.supportsNativeDash; 36135 } 36136 36137 return false; 36138 }; 36139 /** 36140 * VHS is a source handler, not a tech. Make sure attempts to use it 36141 * as one do not cause exceptions. 36142 */ 36143 36144 36145 Vhs.isSupported = function () { 36146 return videojs__default["default"].log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 36147 }; 36148 /** 36149 * A global function for setting an onRequest hook 36150 * 36151 * @param {function} callback for request modifiction 36152 */ 36153 36154 36155 Vhs.xhr.onRequest = function (callback) { 36156 addOnRequestHook(Vhs.xhr, callback); 36157 }; 36158 /** 36159 * A global function for setting an onResponse hook 36160 * 36161 * @param {callback} callback for response data retrieval 36162 */ 36163 36164 36165 Vhs.xhr.onResponse = function (callback) { 36166 addOnResponseHook(Vhs.xhr, callback); 36167 }; 36168 /** 36169 * Deletes a global onRequest callback if it exists 36170 * 36171 * @param {function} callback to delete from the global set 36172 */ 36173 36174 36175 Vhs.xhr.offRequest = function (callback) { 36176 removeOnRequestHook(Vhs.xhr, callback); 36177 }; 36178 /** 36179 * Deletes a global onResponse callback if it exists 36180 * 36181 * @param {function} callback to delete from the global set 36182 */ 36183 36184 36185 Vhs.xhr.offResponse = function (callback) { 36186 removeOnResponseHook(Vhs.xhr, callback); 36187 }; 36188 36189 const Component = videojs__default["default"].getComponent('Component'); 36190 /** 36191 * The Vhs Handler object, where we orchestrate all of the parts 36192 * of VHS to interact with video.js 36193 * 36194 * @class VhsHandler 36195 * @extends videojs.Component 36196 * @param {Object} source the soruce object 36197 * @param {Tech} tech the parent tech object 36198 * @param {Object} options optional and required options 36199 */ 36200 36201 class VhsHandler extends Component { 36202 constructor(source, tech, options) { 36203 super(tech, options.vhs); // if a tech level `initialBandwidth` option was passed 36204 // use that over the VHS level `bandwidth` option 36205 36206 if (typeof options.initialBandwidth === 'number') { 36207 this.options_.bandwidth = options.initialBandwidth; 36208 } 36209 36210 this.logger_ = logger('VhsHandler'); // we need access to the player in some cases, 36211 // so, get it from Video.js via the `playerId` 36212 36213 if (tech.options_ && tech.options_.playerId) { 36214 const _player = videojs__default["default"].getPlayer(tech.options_.playerId); 36215 36216 this.player_ = _player; 36217 } 36218 36219 this.tech_ = tech; 36220 this.source_ = source; 36221 this.stats = {}; 36222 this.ignoreNextSeekingEvent_ = false; 36223 this.setOptions_(); 36224 36225 if (this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) { 36226 tech.overrideNativeAudioTracks(true); 36227 tech.overrideNativeVideoTracks(true); 36228 } else if (this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { 36229 // overriding native VHS only works if audio tracks have been emulated 36230 // error early if we're misconfigured 36231 throw new Error('Overriding native VHS requires emulated tracks. ' + 'See https://git.io/vMpjB'); 36232 } // listen for fullscreenchange events for this player so that we 36233 // can adjust our quality selection quickly 36234 36235 36236 this.on(document, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], event => { 36237 const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; 36238 36239 if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) { 36240 this.playlistController_.fastQualityChange_(); 36241 } else { 36242 // When leaving fullscreen, since the in page pixel dimensions should be smaller 36243 // than full screen, see if there should be a rendition switch down to preserve 36244 // bandwidth. 36245 this.playlistController_.checkABR_(); 36246 } 36247 }); 36248 this.on(this.tech_, 'seeking', function () { 36249 if (this.ignoreNextSeekingEvent_) { 36250 this.ignoreNextSeekingEvent_ = false; 36251 return; 36252 } 36253 36254 this.setCurrentTime(this.tech_.currentTime()); 36255 }); 36256 this.on(this.tech_, 'error', function () { 36257 // verify that the error was real and we are loaded 36258 // enough to have pc loaded. 36259 if (this.tech_.error() && this.playlistController_) { 36260 this.playlistController_.pauseLoading(); 36261 } 36262 }); 36263 this.on(this.tech_, 'play', this.play); 36264 } 36265 /** 36266 * Set VHS options based on options from configuration, as well as partial 36267 * options to be passed at a later time. 36268 * 36269 * @param {Object} options A partial chunk of config options 36270 */ 36271 36272 36273 setOptions_(options = {}) { 36274 this.options_ = merge$1(this.options_, options); // defaults 36275 36276 this.options_.withCredentials = this.options_.withCredentials || false; 36277 this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true; 36278 this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false; 36279 this.options_.usePlayerObjectFit = this.options_.usePlayerObjectFit || false; 36280 this.options_.useBandwidthFromLocalStorage = typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false; 36281 this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false; 36282 this.options_.useNetworkInformationApi = typeof this.options_.useNetworkInformationApi !== 'undefined' ? this.options_.useNetworkInformationApi : true; 36283 this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false; 36284 this.options_.customTagParsers = this.options_.customTagParsers || []; 36285 this.options_.customTagMappers = this.options_.customTagMappers || []; 36286 this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; 36287 this.options_.llhls = this.options_.llhls === false ? false : true; 36288 this.options_.bufferBasedABR = this.options_.bufferBasedABR || false; 36289 36290 if (typeof this.options_.playlistExclusionDuration !== 'number') { 36291 this.options_.playlistExclusionDuration = 60; 36292 } 36293 36294 if (typeof this.options_.bandwidth !== 'number') { 36295 if (this.options_.useBandwidthFromLocalStorage) { 36296 const storedObject = getVhsLocalStorage(); 36297 36298 if (storedObject && storedObject.bandwidth) { 36299 this.options_.bandwidth = storedObject.bandwidth; 36300 this.tech_.trigger({ 36301 type: 'usage', 36302 name: 'vhs-bandwidth-from-local-storage' 36303 }); 36304 } 36305 36306 if (storedObject && storedObject.throughput) { 36307 this.options_.throughput = storedObject.throughput; 36308 this.tech_.trigger({ 36309 type: 'usage', 36310 name: 'vhs-throughput-from-local-storage' 36311 }); 36312 } 36313 } 36314 } // if bandwidth was not set by options or pulled from local storage, start playlist 36315 // selection at a reasonable bandwidth 36316 36317 36318 if (typeof this.options_.bandwidth !== 'number') { 36319 this.options_.bandwidth = Config.INITIAL_BANDWIDTH; 36320 } // If the bandwidth number is unchanged from the initial setting 36321 // then this takes precedence over the enableLowInitialPlaylist option 36322 36323 36324 this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src 36325 36326 ['withCredentials', 'useDevicePixelRatio', 'usePlayerObjectFit', 'customPixelRatio', 'limitRenditionByPlayerDimensions', 'bandwidth', 'customTagParsers', 'customTagMappers', 'cacheEncryptionKeys', 'playlistSelector', 'initialPlaylistSelector', 'bufferBasedABR', 'liveRangeSafeTimeDelta', 'llhls', 'useForcedSubtitles', 'useNetworkInformationApi', 'useDtsForTimestampOffset', 'exactManifestTimings', 'leastPixelDiffSelector'].forEach(option => { 36327 if (typeof this.source_[option] !== 'undefined') { 36328 this.options_[option] = this.source_[option]; 36329 } 36330 }); 36331 this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions; 36332 this.useDevicePixelRatio = this.options_.useDevicePixelRatio; 36333 this.usePlayerObjectFit = this.options_.usePlayerObjectFit; 36334 const customPixelRatio = this.options_.customPixelRatio; // Ensure the custom pixel ratio is a number greater than or equal to 0 36335 36336 if (typeof customPixelRatio === 'number' && customPixelRatio >= 0) { 36337 this.customPixelRatio = customPixelRatio; 36338 } 36339 } // alias for public method to set options 36340 36341 36342 setOptions(options = {}) { 36343 this.setOptions_(options); 36344 } 36345 /** 36346 * called when player.src gets called, handle a new source 36347 * 36348 * @param {Object} src the source object to handle 36349 */ 36350 36351 36352 src(src, type) { 36353 // do nothing if the src is falsey 36354 if (!src) { 36355 return; 36356 } 36357 36358 this.setOptions_(); // add main playlist controller options 36359 36360 this.options_.src = expandDataUri(this.source_.src); 36361 this.options_.tech = this.tech_; 36362 this.options_.externVhs = Vhs; 36363 this.options_.sourceType = simpleTypeFromSourceType(type); // Whenever we seek internally, we should update the tech 36364 36365 this.options_.seekTo = time => { 36366 this.tech_.setCurrentTime(time); 36367 }; // pass player to allow for player level eventing on construction. 36368 36369 36370 this.options_.player_ = this.player_; 36371 this.playlistController_ = new PlaylistController(this.options_); 36372 const playbackWatcherOptions = merge$1({ 36373 liveRangeSafeTimeDelta: SAFE_TIME_DELTA 36374 }, this.options_, { 36375 seekable: () => this.seekable(), 36376 media: () => this.playlistController_.media(), 36377 playlistController: this.playlistController_ 36378 }); 36379 this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions); 36380 this.attachStreamingEventListeners_(); 36381 this.playlistController_.on('error', () => { 36382 const player = videojs__default["default"].players[this.tech_.options_.playerId]; 36383 let error = this.playlistController_.error; 36384 36385 if (typeof error === 'object' && !error.code) { 36386 error.code = 3; 36387 } else if (typeof error === 'string') { 36388 error = { 36389 message: error, 36390 code: 3 36391 }; 36392 } 36393 36394 player.error(error); 36395 }); 36396 const defaultSelector = this.options_.bufferBasedABR ? Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR; // `this` in selectPlaylist should be the VhsHandler for backwards 36397 // compatibility with < v2 36398 36399 this.playlistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : defaultSelector.bind(this); 36400 this.playlistController_.selectInitialPlaylist = Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 36401 36402 this.playlists = this.playlistController_.mainPlaylistLoader_; 36403 this.mediaSource = this.playlistController_.mediaSource; // Proxy assignment of some properties to the main playlist 36404 // controller. Using a custom property for backwards compatibility 36405 // with < v2 36406 36407 Object.defineProperties(this, { 36408 selectPlaylist: { 36409 get() { 36410 return this.playlistController_.selectPlaylist; 36411 }, 36412 36413 set(selectPlaylist) { 36414 this.playlistController_.selectPlaylist = selectPlaylist.bind(this); 36415 } 36416 36417 }, 36418 throughput: { 36419 get() { 36420 return this.playlistController_.mainSegmentLoader_.throughput.rate; 36421 }, 36422 36423 set(throughput) { 36424 this.playlistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value 36425 // for the cumulative average 36426 36427 this.playlistController_.mainSegmentLoader_.throughput.count = 1; 36428 } 36429 36430 }, 36431 bandwidth: { 36432 get() { 36433 let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth; 36434 const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection; 36435 const tenMbpsAsBitsPerSecond = 10e6; 36436 36437 if (this.options_.useNetworkInformationApi && networkInformation) { 36438 // downlink returns Mbps 36439 // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink 36440 const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player 36441 // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that 36442 // high quality streams are not filtered out. 36443 36444 if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) { 36445 playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec); 36446 } else { 36447 playerBandwidthEst = networkInfoBandwidthEstBitsPerSec; 36448 } 36449 } 36450 36451 return playerBandwidthEst; 36452 }, 36453 36454 set(bandwidth) { 36455 this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter 36456 // `count` is set to zero that current value of `rate` isn't included 36457 // in the cumulative average 36458 36459 this.playlistController_.mainSegmentLoader_.throughput = { 36460 rate: 0, 36461 count: 0 36462 }; 36463 } 36464 36465 }, 36466 36467 /** 36468 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 36469 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 36470 * the entire process after that - decryption, transmuxing, and appending - provided 36471 * by `throughput`. 36472 * 36473 * Since the two process are serial, the overall system bandwidth is given by: 36474 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 36475 */ 36476 systemBandwidth: { 36477 get() { 36478 const invBandwidth = 1 / (this.bandwidth || 1); 36479 let invThroughput; 36480 36481 if (this.throughput > 0) { 36482 invThroughput = 1 / this.throughput; 36483 } else { 36484 invThroughput = 0; 36485 } 36486 36487 const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 36488 return systemBitrate; 36489 }, 36490 36491 set() { 36492 videojs__default["default"].log.error('The "systemBandwidth" property is read-only'); 36493 } 36494 36495 } 36496 }); 36497 36498 if (this.options_.bandwidth) { 36499 this.bandwidth = this.options_.bandwidth; 36500 } 36501 36502 if (this.options_.throughput) { 36503 this.throughput = this.options_.throughput; 36504 } 36505 36506 Object.defineProperties(this.stats, { 36507 bandwidth: { 36508 get: () => this.bandwidth || 0, 36509 enumerable: true 36510 }, 36511 mediaRequests: { 36512 get: () => this.playlistController_.mediaRequests_() || 0, 36513 enumerable: true 36514 }, 36515 mediaRequestsAborted: { 36516 get: () => this.playlistController_.mediaRequestsAborted_() || 0, 36517 enumerable: true 36518 }, 36519 mediaRequestsTimedout: { 36520 get: () => this.playlistController_.mediaRequestsTimedout_() || 0, 36521 enumerable: true 36522 }, 36523 mediaRequestsErrored: { 36524 get: () => this.playlistController_.mediaRequestsErrored_() || 0, 36525 enumerable: true 36526 }, 36527 mediaTransferDuration: { 36528 get: () => this.playlistController_.mediaTransferDuration_() || 0, 36529 enumerable: true 36530 }, 36531 mediaBytesTransferred: { 36532 get: () => this.playlistController_.mediaBytesTransferred_() || 0, 36533 enumerable: true 36534 }, 36535 mediaSecondsLoaded: { 36536 get: () => this.playlistController_.mediaSecondsLoaded_() || 0, 36537 enumerable: true 36538 }, 36539 mediaAppends: { 36540 get: () => this.playlistController_.mediaAppends_() || 0, 36541 enumerable: true 36542 }, 36543 mainAppendsToLoadedData: { 36544 get: () => this.playlistController_.mainAppendsToLoadedData_() || 0, 36545 enumerable: true 36546 }, 36547 audioAppendsToLoadedData: { 36548 get: () => this.playlistController_.audioAppendsToLoadedData_() || 0, 36549 enumerable: true 36550 }, 36551 appendsToLoadedData: { 36552 get: () => this.playlistController_.appendsToLoadedData_() || 0, 36553 enumerable: true 36554 }, 36555 timeToLoadedData: { 36556 get: () => this.playlistController_.timeToLoadedData_() || 0, 36557 enumerable: true 36558 }, 36559 buffered: { 36560 get: () => timeRangesToArray(this.tech_.buffered()), 36561 enumerable: true 36562 }, 36563 currentTime: { 36564 get: () => this.tech_.currentTime(), 36565 enumerable: true 36566 }, 36567 currentSource: { 36568 get: () => this.tech_.currentSource_, 36569 enumerable: true 36570 }, 36571 currentTech: { 36572 get: () => this.tech_.name_, 36573 enumerable: true 36574 }, 36575 duration: { 36576 get: () => this.tech_.duration(), 36577 enumerable: true 36578 }, 36579 main: { 36580 get: () => this.playlists.main, 36581 enumerable: true 36582 }, 36583 playerDimensions: { 36584 get: () => this.tech_.currentDimensions(), 36585 enumerable: true 36586 }, 36587 seekable: { 36588 get: () => timeRangesToArray(this.tech_.seekable()), 36589 enumerable: true 36590 }, 36591 timestamp: { 36592 get: () => Date.now(), 36593 enumerable: true 36594 }, 36595 videoPlaybackQuality: { 36596 get: () => this.tech_.getVideoPlaybackQuality(), 36597 enumerable: true 36598 } 36599 }); 36600 this.tech_.one('canplay', this.playlistController_.setupFirstPlay.bind(this.playlistController_)); 36601 this.tech_.on('bandwidthupdate', () => { 36602 if (this.options_.useBandwidthFromLocalStorage) { 36603 updateVhsLocalStorage({ 36604 bandwidth: this.bandwidth, 36605 throughput: Math.round(this.throughput) 36606 }); 36607 } 36608 }); 36609 this.playlistController_.on('selectedinitialmedia', () => { 36610 // Add the manual rendition mix-in to VhsHandler 36611 renditionSelectionMixin(this); 36612 }); 36613 this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => { 36614 this.setupEme_(); 36615 }); // the bandwidth of the primary segment loader is our best 36616 // estimate of overall bandwidth 36617 36618 this.on(this.playlistController_, 'progress', function () { 36619 this.tech_.trigger('progress'); 36620 }); // In the live case, we need to ignore the very first `seeking` event since 36621 // that will be the result of the seek-to-live behavior 36622 36623 this.on(this.playlistController_, 'firstplay', function () { 36624 this.ignoreNextSeekingEvent_ = true; 36625 }); 36626 this.setupQualityLevels_(); // do nothing if the tech has been disposed already 36627 // this can occur if someone sets the src in player.ready(), for instance 36628 36629 if (!this.tech_.el()) { 36630 return; 36631 } 36632 36633 this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource); // If we are playing HLS with MSE in Safari, add source elements for both the blob and manifest URLs. 36634 // The latter will enable Airplay playback on receiver devices. 36635 36636 if ((videojs__default["default"].browser.IS_ANY_SAFARI || videojs__default["default"].browser.IS_IOS) && this.options_.overrideNative && this.options_.sourceType === 'hls' && typeof this.tech_.addSourceElement === 'function') { 36637 this.tech_.addSourceElement(this.mediaSourceUrl_); 36638 this.tech_.addSourceElement(this.source_.src); 36639 } else { 36640 this.tech_.src(this.mediaSourceUrl_); 36641 } 36642 } 36643 36644 createKeySessions_() { 36645 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 36646 this.logger_('waiting for EME key session creation'); 36647 waitForKeySessionCreation({ 36648 player: this.player_, 36649 sourceKeySystems: this.source_.keySystems, 36650 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(), 36651 mainPlaylists: this.playlists.main.playlists 36652 }).then(() => { 36653 this.logger_('created EME key session'); 36654 this.playlistController_.sourceUpdater_.initializedEme(); 36655 }).catch(err => { 36656 this.logger_('error while creating EME key session', err); 36657 this.player_.error({ 36658 message: 'Failed to initialize media keys for EME', 36659 code: 3 36660 }); 36661 }); 36662 } 36663 36664 handleWaitingForKey_() { 36665 // If waitingforkey is fired, it's possible that the data that's necessary to retrieve 36666 // the key is in the manifest. While this should've happened on initial source load, it 36667 // may happen again in live streams where the keys change, and the manifest info 36668 // reflects the update. 36669 // 36670 // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's 36671 // already requested keys for, we don't have to worry about this generating extraneous 36672 // requests. 36673 this.logger_('waitingforkey fired, attempting to create any new key sessions'); 36674 this.createKeySessions_(); 36675 } 36676 /** 36677 * If necessary and EME is available, sets up EME options and waits for key session 36678 * creation. 36679 * 36680 * This function also updates the source updater so taht it can be used, as for some 36681 * browsers, EME must be configured before content is appended (if appending unencrypted 36682 * content before encrypted content). 36683 */ 36684 36685 36686 setupEme_() { 36687 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 36688 const didSetupEmeOptions = setupEmeOptions({ 36689 player: this.player_, 36690 sourceKeySystems: this.source_.keySystems, 36691 media: this.playlists.media(), 36692 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media() 36693 }); 36694 this.player_.tech_.on('keystatuschange', e => { 36695 this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status); 36696 }); 36697 this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this); 36698 this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_); 36699 36700 if (!didSetupEmeOptions) { 36701 // If EME options were not set up, we've done all we could to initialize EME. 36702 this.playlistController_.sourceUpdater_.initializedEme(); 36703 return; 36704 } 36705 36706 this.createKeySessions_(); 36707 } 36708 /** 36709 * Initializes the quality levels and sets listeners to update them. 36710 * 36711 * @method setupQualityLevels_ 36712 * @private 36713 */ 36714 36715 36716 setupQualityLevels_() { 36717 const player = videojs__default["default"].players[this.tech_.options_.playerId]; // if there isn't a player or there isn't a qualityLevels plugin 36718 // or qualityLevels_ listeners have already been setup, do nothing. 36719 36720 if (!player || !player.qualityLevels || this.qualityLevels_) { 36721 return; 36722 } 36723 36724 this.qualityLevels_ = player.qualityLevels(); 36725 this.playlistController_.on('selectedinitialmedia', () => { 36726 handleVhsLoadedMetadata(this.qualityLevels_, this); 36727 }); 36728 this.playlists.on('mediachange', () => { 36729 handleVhsMediaChange(this.qualityLevels_, this.playlists); 36730 }); 36731 } 36732 /** 36733 * return the version 36734 */ 36735 36736 36737 static version() { 36738 return { 36739 '@videojs/http-streaming': version$4, 36740 'mux.js': version$3, 36741 'mpd-parser': version$2, 36742 'm3u8-parser': version$1, 36743 'aes-decrypter': version 36744 }; 36745 } 36746 /** 36747 * return the version 36748 */ 36749 36750 36751 version() { 36752 return this.constructor.version(); 36753 } 36754 36755 canChangeType() { 36756 return SourceUpdater.canChangeType(); 36757 } 36758 /** 36759 * Begin playing the video. 36760 */ 36761 36762 36763 play() { 36764 this.playlistController_.play(); 36765 } 36766 /** 36767 * a wrapper around the function in PlaylistController 36768 */ 36769 36770 36771 setCurrentTime(currentTime) { 36772 this.playlistController_.setCurrentTime(currentTime); 36773 } 36774 /** 36775 * a wrapper around the function in PlaylistController 36776 */ 36777 36778 36779 duration() { 36780 return this.playlistController_.duration(); 36781 } 36782 /** 36783 * a wrapper around the function in PlaylistController 36784 */ 36785 36786 36787 seekable() { 36788 return this.playlistController_.seekable(); 36789 } 36790 /** 36791 * Abort all outstanding work and cleanup. 36792 */ 36793 36794 36795 dispose() { 36796 if (this.playbackWatcher_) { 36797 this.playbackWatcher_.dispose(); 36798 } 36799 36800 if (this.playlistController_) { 36801 this.playlistController_.dispose(); 36802 } 36803 36804 if (this.qualityLevels_) { 36805 this.qualityLevels_.dispose(); 36806 } 36807 36808 if (this.tech_ && this.tech_.vhs) { 36809 delete this.tech_.vhs; 36810 } 36811 36812 if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) { 36813 window.URL.revokeObjectURL(this.mediaSourceUrl_); 36814 this.mediaSourceUrl_ = null; 36815 } 36816 36817 if (this.tech_) { 36818 this.tech_.off('waitingforkey', this.handleWaitingForKey_); 36819 } 36820 36821 super.dispose(); 36822 } 36823 36824 convertToProgramTime(time, callback) { 36825 return getProgramTime({ 36826 playlist: this.playlistController_.media(), 36827 time, 36828 callback 36829 }); 36830 } // the player must be playing before calling this 36831 36832 36833 seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) { 36834 return seekToProgramTime({ 36835 programTime, 36836 playlist: this.playlistController_.media(), 36837 retryCount, 36838 pauseAfterSeek, 36839 seekTo: this.options_.seekTo, 36840 tech: this.options_.tech, 36841 callback 36842 }); 36843 } 36844 /** 36845 * Adds the onRequest, onResponse, offRequest and offResponse functions 36846 * to the VhsHandler xhr Object. 36847 */ 36848 36849 36850 setupXhrHooks_() { 36851 /** 36852 * A player function for setting an onRequest hook 36853 * 36854 * @param {function} callback for request modifiction 36855 */ 36856 this.xhr.onRequest = callback => { 36857 addOnRequestHook(this.xhr, callback); 36858 }; 36859 /** 36860 * A player function for setting an onResponse hook 36861 * 36862 * @param {callback} callback for response data retrieval 36863 */ 36864 36865 36866 this.xhr.onResponse = callback => { 36867 addOnResponseHook(this.xhr, callback); 36868 }; 36869 /** 36870 * Deletes a player onRequest callback if it exists 36871 * 36872 * @param {function} callback to delete from the player set 36873 */ 36874 36875 36876 this.xhr.offRequest = callback => { 36877 removeOnRequestHook(this.xhr, callback); 36878 }; 36879 /** 36880 * Deletes a player onResponse callback if it exists 36881 * 36882 * @param {function} callback to delete from the player set 36883 */ 36884 36885 36886 this.xhr.offResponse = callback => { 36887 removeOnResponseHook(this.xhr, callback); 36888 }; // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks. 36889 // This allows hooks to be set before the source is set to vhs when handleSource is called. 36890 36891 36892 this.player_.trigger('xhr-hooks-ready'); 36893 } 36894 36895 attachStreamingEventListeners_() { 36896 const playlistControllerEvents = ['seekablerangeschanged', 'bufferedrangeschanged', 'contentsteeringloadstart', 'contentsteeringloadcomplete', 'contentsteeringparsed']; 36897 const playbackWatcher = ['gapjumped', 'playedrangeschanged']; // re-emit streaming events and payloads on the player. 36898 36899 playlistControllerEvents.forEach(eventName => { 36900 this.playlistController_.on(eventName, metadata => { 36901 this.player_.trigger(_extends({}, metadata)); 36902 }); 36903 }); 36904 playbackWatcher.forEach(eventName => { 36905 this.playbackWatcher_.on(eventName, metadata => { 36906 this.player_.trigger(_extends({}, metadata)); 36907 }); 36908 }); 36909 } 36910 36911 } 36912 /** 36913 * The Source Handler object, which informs video.js what additional 36914 * MIME types are supported and sets up playback. It is registered 36915 * automatically to the appropriate tech based on the capabilities of 36916 * the browser it is running in. It is not necessary to use or modify 36917 * this object in normal usage. 36918 */ 36919 36920 36921 const VhsSourceHandler = { 36922 name: 'videojs-http-streaming', 36923 VERSION: version$4, 36924 36925 canHandleSource(srcObj, options = {}) { 36926 const localOptions = merge$1(videojs__default["default"].options, options); // If not opting to experimentalUseMMS, and playback is only supported with MediaSource, cannot handle source 36927 36928 if (!localOptions.vhs.experimentalUseMMS && !browserSupportsCodec('avc1.4d400d,mp4a.40.2', false)) { 36929 return false; 36930 } 36931 36932 return VhsSourceHandler.canPlayType(srcObj.type, localOptions); 36933 }, 36934 36935 handleSource(source, tech, options = {}) { 36936 const localOptions = merge$1(videojs__default["default"].options, options); 36937 tech.vhs = new VhsHandler(source, tech, localOptions); 36938 tech.vhs.xhr = xhrFactory(); 36939 tech.vhs.setupXhrHooks_(); 36940 tech.vhs.src(source.src, source.type); 36941 return tech.vhs; 36942 }, 36943 36944 canPlayType(type, options) { 36945 const simpleType = simpleTypeFromSourceType(type); 36946 36947 if (!simpleType) { 36948 return ''; 36949 } 36950 36951 const overrideNative = VhsSourceHandler.getOverrideNative(options); 36952 const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType); 36953 const canUseMsePlayback = !supportsTypeNatively || overrideNative; 36954 return canUseMsePlayback ? 'maybe' : ''; 36955 }, 36956 36957 getOverrideNative(options = {}) { 36958 const { 36959 vhs = {} 36960 } = options; 36961 const defaultOverrideNative = !(videojs__default["default"].browser.IS_ANY_SAFARI || videojs__default["default"].browser.IS_IOS); 36962 const { 36963 overrideNative = defaultOverrideNative 36964 } = vhs; 36965 return overrideNative; 36966 } 36967 36968 }; 36969 /** 36970 * Check to see if either the native MediaSource or ManagedMediaSource 36971 * objectx exist and support an MP4 container with both H.264 video 36972 * and AAC-LC audio. 36973 * 36974 * @return {boolean} if native media sources are supported 36975 */ 36976 36977 const supportsNativeMediaSources = () => { 36978 return browserSupportsCodec('avc1.4d400d,mp4a.40.2', true); 36979 }; // register source handlers with the appropriate techs 36980 36981 36982 if (supportsNativeMediaSources()) { 36983 videojs__default["default"].getTech('Html5').registerSourceHandler(VhsSourceHandler, 0); 36984 } 36985 36986 videojs__default["default"].VhsHandler = VhsHandler; 36987 videojs__default["default"].VhsSourceHandler = VhsSourceHandler; 36988 videojs__default["default"].Vhs = Vhs; 36989 36990 if (!videojs__default["default"].use) { 36991 videojs__default["default"].registerComponent('Vhs', Vhs); 36992 } 36993 36994 videojs__default["default"].options.vhs = videojs__default["default"].options.vhs || {}; 36995 36996 if (!videojs__default["default"].getPlugin || !videojs__default["default"].getPlugin('reloadSourceOnError')) { 36997 videojs__default["default"].registerPlugin('reloadSourceOnError', reloadSourceOnError); 36998 } 36999 37000 exports.LOCAL_STORAGE_KEY = LOCAL_STORAGE_KEY; 37001 exports.Vhs = Vhs; 37002 exports.VhsHandler = VhsHandler; 37003 exports.VhsSourceHandler = VhsSourceHandler; 37004 exports.emeKeySystems = emeKeySystems; 37005 exports.expandDataUri = expandDataUri; 37006 exports.getAllPsshKeySystemsOptions = getAllPsshKeySystemsOptions; 37007 exports.setupEmeOptions = setupEmeOptions; 37008 exports.simpleTypeFromSourceType = simpleTypeFromSourceType; 37009 exports.waitForKeySessionCreation = waitForKeySessionCreation; 37010 37011 Object.defineProperty(exports, '__esModule', { value: true }); 37012 37013}));
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.