1/*! @name @videojs/http-streaming @version 3.10.0 @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 urlToolkit = {exports: {}}; 31 32 (function (module, exports) { 33 // see https://tools.ietf.org/html/rfc1808 34 (function (root) { 35 var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/?#]*)?((?:[^\/?#]*\/)*[^;?#]*)?(;[^?#]*)?(\?[^#]*)?(#[^]*)?$/; 36 var FIRST_SEGMENT_REGEX = /^([^\/?#]*)([^]*)$/; 37 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 38 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g; 39 var URLToolkit = { 40 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 41 // E.g 42 // With opts.alwaysNormalize = false (default, spec compliant) 43 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 44 // With opts.alwaysNormalize = true (not spec compliant) 45 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 46 buildAbsoluteURL: function (baseURL, relativeURL, opts) { 47 opts = opts || {}; // remove any remaining space and CRLF 48 49 baseURL = baseURL.trim(); 50 relativeURL = relativeURL.trim(); 51 52 if (!relativeURL) { 53 // 2a) If the embedded URL is entirely empty, it inherits the 54 // entire base URL (i.e., is set equal to the base URL) 55 // and we are done. 56 if (!opts.alwaysNormalize) { 57 return baseURL; 58 } 59 60 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 61 62 if (!basePartsForNormalise) { 63 throw new Error('Error trying to parse base URL.'); 64 } 65 66 basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); 67 return URLToolkit.buildURLFromParts(basePartsForNormalise); 68 } 69 70 var relativeParts = URLToolkit.parseURL(relativeURL); 71 72 if (!relativeParts) { 73 throw new Error('Error trying to parse relative URL.'); 74 } 75 76 if (relativeParts.scheme) { 77 // 2b) If the embedded URL starts with a scheme name, it is 78 // interpreted as an absolute URL and we are done. 79 if (!opts.alwaysNormalize) { 80 return relativeURL; 81 } 82 83 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 84 return URLToolkit.buildURLFromParts(relativeParts); 85 } 86 87 var baseParts = URLToolkit.parseURL(baseURL); 88 89 if (!baseParts) { 90 throw new Error('Error trying to parse base URL.'); 91 } 92 93 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 94 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 95 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 96 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 97 baseParts.netLoc = pathParts[1]; 98 baseParts.path = pathParts[2]; 99 } 100 101 if (baseParts.netLoc && !baseParts.path) { 102 baseParts.path = '/'; 103 } 104 105 var builtParts = { 106 // 2c) Otherwise, the embedded URL inherits the scheme of 107 // the base URL. 108 scheme: baseParts.scheme, 109 netLoc: relativeParts.netLoc, 110 path: null, 111 params: relativeParts.params, 112 query: relativeParts.query, 113 fragment: relativeParts.fragment 114 }; 115 116 if (!relativeParts.netLoc) { 117 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 118 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 119 // (if any) of the base URL. 120 builtParts.netLoc = baseParts.netLoc; // 4) If the embedded URL path is preceded by a slash "/", the 121 // path is not relative and we skip to Step 7. 122 123 if (relativeParts.path[0] !== '/') { 124 if (!relativeParts.path) { 125 // 5) If the embedded URL path is empty (and not preceded by a 126 // slash), then the embedded URL inherits the base URL path 127 builtParts.path = baseParts.path; // 5a) if the embedded URL's <params> is non-empty, we skip to 128 // step 7; otherwise, it inherits the <params> of the base 129 // URL (if any) and 130 131 if (!relativeParts.params) { 132 builtParts.params = baseParts.params; // 5b) if the embedded URL's <query> is non-empty, we skip to 133 // step 7; otherwise, it inherits the <query> of the base 134 // URL (if any) and we skip to step 7. 135
vendor: 4,693 bytes, lines 136-259
136 if (!relativeParts.query) { 137 builtParts.query = baseParts.query; 138 } 139 } 140 } else { 141 // 6) The last segment of the base URL's path (anything 142 // following the rightmost slash "/", or the entire path if no 143 // slash is present) is removed and the embedded URL's path is 144 // appended in its place. 145 var baseURLPath = baseParts.path; 146 var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; 147 builtParts.path = URLToolkit.normalizePath(newPath); 148 } 149 } 150 } 151 152 if (builtParts.path === null) { 153 builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; 154 } 155 156 return URLToolkit.buildURLFromParts(builtParts); 157 }, 158 parseURL: function (url) { 159 var parts = URL_REGEX.exec(url); 160 161 if (!parts) { 162 return null; 163 } 164 165 return { 166 scheme: parts[1] || '', 167 netLoc: parts[2] || '', 168 path: parts[3] || '', 169 params: parts[4] || '', 170 query: parts[5] || '', 171 fragment: parts[6] || '' 172 }; 173 }, 174 normalizePath: function (path) { 175 // The following operations are 176 // then applied, in order, to the new path: 177 // 6a) All occurrences of "./", where "." is a complete path 178 // segment, are removed. 179 // 6b) If the path ends with "." as a complete path segment, 180 // that "." is removed. 181 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); // 6c) All occurrences of "<segment>/../", where <segment> is a 182 // complete path segment not equal to "..", are removed. 183 // Removal of these path segments is performed iteratively, 184 // removing the leftmost matching pattern on each iteration, 185 // until no matching pattern remains. 186 // 6d) If the path ends with "<segment>/..", where <segment> is a 187 // complete path segment not equal to "..", that 188 // "<segment>/.." is removed. 189 190 while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} 191 192 return path.split('').reverse().join(''); 193 }, 194 buildURLFromParts: function (parts) { 195 return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; 196 } 197 }; 198 module.exports = URLToolkit; 199 })(); 200 })(urlToolkit); 201 202 var URLToolkit = urlToolkit.exports; 203 204 var DEFAULT_LOCATION = 'http://example.com'; 205 206 var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) { 207 // return early if we don't need to resolve 208 if (/^[a-z]+:/i.test(relativeUrl)) { 209 return relativeUrl; 210 } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location 211 212 213 if (/^data:/.test(baseUrl)) { 214 baseUrl = window.location && window.location.href || ''; 215 } // IE11 supports URL but not the URL constructor 216 // feature detect the behavior we want 217 218 219 var nativeURL = typeof window.URL === 'function'; 220 var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node) 221 // and if baseUrl isn't an absolute url 222 223 var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location 224 225 if (nativeURL) { 226 baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION); 227 } else if (!/\/\//i.test(baseUrl)) { 228 baseUrl = URLToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl); 229 } 230 231 if (nativeURL) { 232 var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol 233 // and if we're location-less, remove the location 234 // otherwise, return the url unmodified 235 236 if (removeLocation) { 237 return newUrl.href.slice(DEFAULT_LOCATION.length); 238 } else if (protocolLess) { 239 return newUrl.href.slice(newUrl.protocol.length); 240 } 241 242 return newUrl.href; 243 } 244 245 return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl); 246 }; 247 248 /** 249 * @file resolve-url.js - Handling how URLs are resolved and manipulated 250 */ 251 const resolveUrl = resolveUrl$1; 252 /** 253 * If the xhr request was redirected, return the responseURL, otherwise, 254 * return the original url. 255 * 256 * @api private 257 * 258 * @param {string} url - an url being requested 259 * @param {XMLHttpRequest}
vendor: 5,880 bytes, lines 259-483
259 req - xhr request result 260 * 261 * @return {string} 262 */ 263 264 const resolveManifestRedirect = (url, req) => { 265 // To understand how the responseURL below is set and generated: 266 // - https://fetch.spec.whatwg.org/#concept-response-url 267 // - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 268 if (req && req.responseURL && url !== req.responseURL) { 269 return req.responseURL; 270 } 271 272 return url; 273 }; 274 275 const logger = source => { 276 if (videojs__default["default"].log.debug) { 277 return videojs__default["default"].log.debug.bind(videojs__default["default"], 'VHS:', `${source} >`); 278 } 279 280 return function () {}; 281 }; 282 283 /** 284 * @file stream.js 285 */ 286 287 /** 288 * A lightweight readable stream implemention that handles event dispatching. 289 * 290 * @class Stream 291 */ 292 var Stream = /*#__PURE__*/function () { 293 function Stream() { 294 this.listeners = {}; 295 } 296 /** 297 * Add a listener for a specified event type. 298 * 299 * @param {string} type the event name 300 * @param {Function} listener the callback to be invoked when an event of 301 * the specified type occurs 302 */ 303 304 305 var _proto = Stream.prototype; 306 307 _proto.on = function on(type, listener) { 308 if (!this.listeners[type]) { 309 this.listeners[type] = []; 310 } 311 312 this.listeners[type].push(listener); 313 } 314 /** 315 * Remove a listener for a specified event type. 316 * 317 * @param {string} type the event name 318 * @param {Function} listener a function previously registered for this 319 * type of event through `on` 320 * @return {boolean} if we could turn it off or not 321 */ 322 ; 323 324 _proto.off = function off(type, listener) { 325 if (!this.listeners[type]) { 326 return false; 327 } 328 329 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 330 // In Video.js we slice listener functions 331 // on trigger so that it does not mess up the order 332 // while we loop through. 333 // 334 // Here we slice on off so that the loop in trigger 335 // can continue using it's old reference to loop without 336 // messing up the order. 337 338 this.listeners[type] = this.listeners[type].slice(0); 339 this.listeners[type].splice(index, 1); 340 return index > -1; 341 } 342 /** 343 * Trigger an event of the specified type on this stream. Any additional 344 * arguments to this function are passed as parameters to event listeners. 345 * 346 * @param {string} type the event name 347 */ 348 ; 349 350 _proto.trigger = function trigger(type) { 351 var callbacks = this.listeners[type]; 352 353 if (!callbacks) { 354 return; 355 } // Slicing the arguments on every invocation of this method 356 // can add a significant amount of overhead. Avoid the 357 // intermediate object creation for the common case of a 358 // single callback argument 359 360 361 if (arguments.length === 2) { 362 var length = callbacks.length; 363 364 for (var i = 0; i < length; ++i) { 365 callbacks[i].call(this, arguments[1]); 366 } 367 } else { 368 var args = Array.prototype.slice.call(arguments, 1); 369 var _length = callbacks.length; 370 371 for (var _i = 0; _i < _length; ++_i) { 372 callbacks[_i].apply(this, args); 373 } 374 } 375 } 376 /** 377 * Destroys the stream and cleans up. 378 */ 379 ; 380 381 _proto.dispose = function dispose() { 382 this.listeners = {}; 383 } 384 /** 385 * Forwards all `data` events on this stream to the destination stream. The 386 * destination stream should provide a method `push` to receive the data 387 * events as they arrive. 388 * 389 * @param {Stream} destination the stream that will receive all `data` events 390 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 391 */ 392 ; 393 394 _proto.pipe = function pipe(destination) { 395 this.on('data', function (data) { 396 destination.push(data); 397 }); 398 }; 399 400 return Stream; 401 }(); 402 403 var atob = function atob(s) { 404 return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary'); 405 }; 406 407 function decodeB64ToUint8Array(b64Text) { 408 var decodedString = atob(b64Text); 409 var array = new Uint8Array(decodedString.length); 410 411 for (var i = 0; i < decodedString.length; i++) { 412 array[i] = decodedString.charCodeAt(i); 413 } 414 415 return array; 416 } 417 418 /*! @name m3u8-parser @version 7.1.0 @license Apache-2.0 */ 419 /** 420 * @file m3u8/line-stream.js 421 */ 422 423 /** 424 * A stream that buffers string input and generates a `data` event for each 425 * line. 426 * 427 * @class LineStream 428 * @extends Stream 429 */ 430 431 class LineStream extends Stream { 432 constructor() { 433 super(); 434 this.buffer = ''; 435 } 436 /** 437 * Add new data to be parsed. 438 * 439 * @param {string} data the text to process 440 */ 441 442 443 push(data) { 444 let nextNewline; 445 this.buffer += data; 446 nextNewline = this.buffer.indexOf('\n'); 447 448 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { 449 this.trigger('data', this.buffer.substring(0, nextNewline)); 450 this.buffer = this.buffer.substring(nextNewline + 1); 451 } 452 } 453 454 } 455 456 const TAB = String.fromCharCode(0x09); 457 458 const parseByterange = function (byterangeString) { 459 // optionally match and capture 0+ digits before `@` 460 // optionally match and capture 0+ digits after `@` 461 const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || ''); 462 const result = {}; 463 464 if (match[1]) { 465 result.length = parseInt(match[1], 10); 466 } 467 468 if (match[2]) { 469 result.offset = parseInt(match[2], 10); 470 } 471 472 return result; 473 }; 474 /** 475 * "forgiving" attribute list psuedo-grammar: 476 * attributes -> keyvalue (',' keyvalue)* 477 * keyvalue -> key '=' value 478 * key -> [^=]* 479 * value -> '"' [^"]* '"' | [^,]* 480 */ 481 482 483 const attributeSeparator = function () {
vendor: 26,130 bytes, lines 484-1392
484 const key = '[^=]*'; 485 const value = '"[^"]*"|[^,]*'; 486 const keyvalue = '(?:' + key + ')=(?:' + value + ')'; 487 return new RegExp('(?:^|,)(' + keyvalue + ')'); 488 }; 489 /** 490 * Parse attributes from a line given the separator 491 * 492 * @param {string} attributes the attribute line to parse 493 */ 494 495 496 const parseAttributes$1 = function (attributes) { 497 const result = {}; 498 499 if (!attributes) { 500 return result; 501 } // split the string using attributes as the separator 502 503 504 const attrs = attributes.split(attributeSeparator()); 505 let i = attrs.length; 506 let attr; 507 508 while (i--) { 509 // filter out unmatched portions of the string 510 if (attrs[i] === '') { 511 continue; 512 } // split the key and value 513 514 515 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value 516 517 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 518 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 519 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 520 result[attr[0]] = attr[1]; 521 } 522 523 return result; 524 }; 525 /** 526 * A line-level M3U8 parser event stream. It expects to receive input one 527 * line at a time and performs a context-free parse of its contents. A stream 528 * interpretation of a manifest can be useful if the manifest is expected to 529 * be too large to fit comfortably into memory or the entirety of the input 530 * is not immediately available. Otherwise, it's probably much easier to work 531 * with a regular `Parser` object. 532 * 533 * Produces `data` events with an object that captures the parser's 534 * interpretation of the input. That object has a property `tag` that is one 535 * of `uri`, `comment`, or `tag`. URIs only have a single additional 536 * property, `line`, which captures the entirety of the input without 537 * interpretation. Comments similarly have a single additional property 538 * `text` which is the input without the leading `#`. 539 * 540 * Tags always have a property `tagType` which is the lower-cased version of 541 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, 542 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 543 * tags are given the tag type `unknown` and a single additional property 544 * `data` with the remainder of the input. 545 * 546 * @class ParseStream 547 * @extends Stream 548 */ 549 550 551 class ParseStream extends Stream { 552 constructor() { 553 super(); 554 this.customParsers = []; 555 this.tagMappers = []; 556 } 557 /** 558 * Parses an additional line of input. 559 * 560 * @param {string} line a single line of an M3U8 file to parse 561 */ 562 563 564 push(line) { 565 let match; 566 let event; // strip whitespace 567 568 line = line.trim(); 569 570 if (line.length === 0) { 571 // ignore empty lines 572 return; 573 } // URIs 574 575 576 if (line[0] !== '#') { 577 this.trigger('data', { 578 type: 'uri', 579 uri: line 580 }); 581 return; 582 } // map tags 583 584 585 const newLines = this.tagMappers.reduce((acc, mapper) => { 586 const mappedLine = mapper(line); // skip if unchanged 587 588 if (mappedLine === line) { 589 return acc; 590 } 591 592 return acc.concat([mappedLine]); 593 }, [line]); 594 newLines.forEach(newLine => { 595 for (let i = 0; i < this.customParsers.length; i++) { 596 if (this.customParsers[i].call(this, newLine)) { 597 return; 598 } 599 } // Comments 600 601 602 if (newLine.indexOf('#EXT') !== 0) { 603 this.trigger('data', { 604 type: 'comment', 605 text: newLine.slice(1) 606 }); 607 return; 608 } // strip off any carriage returns here so the regex matching 609 // doesn't have to account for them. 610 611 612 newLine = newLine.replace('\r', ''); // Tags 613 614 match = /^#EXTM3U/.exec(newLine); 615 616 if (match) { 617 this.trigger('data', { 618 type: 'tag', 619 tagType: 'm3u' 620 }); 621 return; 622 } 623 624 match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine); 625 626 if (match) { 627 event = { 628 type: 'tag', 629 tagType: 'inf' 630 }; 631 632 if (match[1]) { 633 event.duration = parseFloat(match[1]); 634 } 635 636 if (match[2]) { 637 event.title = match[2]; 638 } 639 640 this.trigger('data', event); 641 return; 642 } 643 644 match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine); 645 646 if (match) { 647 event = { 648 type: 'tag', 649 tagType: 'targetduration' 650 }; 651 652 if (match[1]) { 653 event.duration = parseInt(match[1], 10); 654 } 655 656 this.trigger('data', event); 657 return; 658 } 659 660 match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine); 661 662 if (match) { 663 event = { 664 type: 'tag', 665 tagType: 'version' 666 }; 667 668 if (match[1]) { 669 event.version = parseInt(match[1], 10); 670 } 671 672 this.trigger('data', event); 673 return; 674 } 675 676 match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine); 677 678 if (match) { 679 event = { 680 type: 'tag', 681 tagType: 'media-sequence' 682 }; 683 684 if (match[1]) { 685 event.number = parseInt(match[1], 10); 686 } 687 688 this.trigger('data', event); 689 return; 690 } 691 692 match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine); 693 694 if (match) { 695 event = { 696 type: 'tag', 697 tagType: 'discontinuity-sequence' 698 }; 699 700 if (match[1]) { 701 event.number = parseInt(match[1], 10); 702 } 703 704 this.trigger('data', event); 705 return; 706 } 707 708 match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine); 709 710 if (match) { 711 event = { 712 type: 'tag', 713 tagType: 'playlist-type' 714 }; 715 716 if (match[1]) { 717 event.playlistType = match[1]; 718 } 719 720 this.trigger('data', event); 721 return; 722 } 723 724 match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine); 725 726 if (match) { 727 event = _extends(parseByterange(match[1]), { 728 type: 'tag', 729 tagType: 'byterange' 730 }); 731 this.trigger('data', event); 732 return; 733 } 734 735 match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine); 736 737 if (match) { 738 event = { 739 type: 'tag', 740 tagType: 'allow-cache' 741 }; 742 743 if (match[1]) { 744 event.allowed = !/NO/.test(match[1]); 745 } 746 747 this.trigger('data', event); 748 return; 749 } 750 751 match = /^#EXT-X-MAP:(.*)$/.exec(newLine); 752 753 if (match) { 754 event = { 755 type: 'tag', 756 tagType: 'map' 757 }; 758 759 if (match[1]) { 760 const attributes = parseAttributes$1(match[1]); 761 762 if (attributes.URI) { 763 event.uri = attributes.URI; 764 } 765 766 if (attributes.BYTERANGE) { 767 event.byterange = parseByterange(attributes.BYTERANGE); 768 } 769 } 770 771 this.trigger('data', event); 772 return; 773 } 774 775 match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine); 776 777 if (match) { 778 event = { 779 type: 'tag', 780 tagType: 'stream-inf' 781 }; 782 783 if (match[1]) { 784 event.attributes = parseAttributes$1(match[1]); 785 786 if (event.attributes.RESOLUTION) { 787 const split = event.attributes.RESOLUTION.split('x'); 788 const resolution = {}; 789 790 if (split[0]) { 791 resolution.width = parseInt(split[0], 10); 792 } 793 794 if (split[1]) { 795 resolution.height = parseInt(split[1], 10); 796 } 797 798 event.attributes.RESOLUTION = resolution; 799 } 800 801 if (event.attributes.BANDWIDTH) { 802 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 803 } 804 805 if (event.attributes['FRAME-RATE']) { 806 event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']); 807 } 808 809 if (event.attributes['PROGRAM-ID']) { 810 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); 811 } 812 } 813 814 this.trigger('data', event); 815 return; 816 } 817 818 match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine); 819 820 if (match) { 821 event = { 822 type: 'tag', 823 tagType: 'media' 824 }; 825 826 if (match[1]) { 827 event.attributes = parseAttributes$1(match[1]); 828 } 829 830 this.trigger('data', event); 831 return; 832 } 833 834 match = /^#EXT-X-ENDLIST/.exec(newLine); 835 836 if (match) { 837 this.trigger('data', { 838 type: 'tag', 839 tagType: 'endlist' 840 }); 841 return; 842 } 843 844 match = /^#EXT-X-DISCONTINUITY/.exec(newLine); 845 846 if (match) { 847 this.trigger('data', { 848 type: 'tag', 849 tagType: 'discontinuity' 850 }); 851 return; 852 } 853 854 match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine); 855 856 if (match) { 857 event = { 858 type: 'tag', 859 tagType: 'program-date-time' 860 }; 861 862 if (match[1]) { 863 event.dateTimeString = match[1]; 864 event.dateTimeObject = new Date(match[1]); 865 } 866 867 this.trigger('data', event); 868 return; 869 } 870 871 match = /^#EXT-X-KEY:(.*)$/.exec(newLine); 872 873 if (match) { 874 event = { 875 type: 'tag', 876 tagType: 'key' 877 }; 878 879 if (match[1]) { 880 event.attributes = parseAttributes$1(match[1]); // parse the IV string into a Uint32Array 881 882 if (event.attributes.IV) { 883 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { 884 event.attributes.IV = event.attributes.IV.substring(2); 885 } 886 887 event.attributes.IV = event.attributes.IV.match(/.{8}/g); 888 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); 889 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); 890 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); 891 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); 892 event.attributes.IV = new Uint32Array(event.attributes.IV); 893 } 894 } 895 896 this.trigger('data', event); 897 return; 898 } 899 900 match = /^#EXT-X-START:(.*)$/.exec(newLine); 901 902 if (match) { 903 event = { 904 type: 'tag', 905 tagType: 'start' 906 }; 907 908 if (match[1]) { 909 event.attributes = parseAttributes$1(match[1]); 910 event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); 911 event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); 912 } 913 914 this.trigger('data', event); 915 return; 916 } 917 918 match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine); 919 920 if (match) { 921 event = { 922 type: 'tag', 923 tagType: 'cue-out-cont' 924 }; 925 926 if (match[1]) { 927 event.data = match[1]; 928 } else { 929 event.data = ''; 930 } 931 932 this.trigger('data', event); 933 return; 934 } 935 936 match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine); 937 938 if (match) { 939 event = { 940 type: 'tag', 941 tagType: 'cue-out' 942 }; 943 944 if (match[1]) { 945 event.data = match[1]; 946 } else { 947 event.data = ''; 948 } 949 950 this.trigger('data', event); 951 return; 952 } 953 954 match = /^#EXT-X-CUE-IN:(.*)?$/.exec(newLine); 955 956 if (match) { 957 event = { 958 type: 'tag', 959 tagType: 'cue-in' 960 }; 961 962 if (match[1]) { 963 event.data = match[1]; 964 } else { 965 event.data = ''; 966 } 967 968 this.trigger('data', event); 969 return; 970 } 971 972 match = /^#EXT-X-SKIP:(.*)$/.exec(newLine); 973 974 if (match && match[1]) { 975 event = { 976 type: 'tag', 977 tagType: 'skip' 978 }; 979 event.attributes = parseAttributes$1(match[1]); 980 981 if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) { 982 event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10); 983 } 984 985 if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) { 986 event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB); 987 } 988 989 this.trigger('data', event); 990 return; 991 } 992 993 match = /^#EXT-X-PART:(.*)$/.exec(newLine); 994 995 if (match && match[1]) { 996 event = { 997 type: 'tag', 998 tagType: 'part' 999 }; 1000 event.attributes = parseAttributes$1(match[1]); 1001 ['DURATION'].forEach(function (key) { 1002 if (event.attributes.hasOwnProperty(key)) { 1003 event.attributes[key] = parseFloat(event.attributes[key]); 1004 } 1005 }); 1006 ['INDEPENDENT', 'GAP'].forEach(function (key) { 1007 if (event.attributes.hasOwnProperty(key)) { 1008 event.attributes[key] = /YES/.test(event.attributes[key]); 1009 } 1010 }); 1011 1012 if (event.attributes.hasOwnProperty('BYTERANGE')) { 1013 event.attributes.byterange = parseByterange(event.attributes.BYTERANGE); 1014 } 1015 1016 this.trigger('data', event); 1017 return; 1018 } 1019 1020 match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine); 1021 1022 if (match && match[1]) { 1023 event = { 1024 type: 'tag', 1025 tagType: 'server-control' 1026 }; 1027 event.attributes = parseAttributes$1(match[1]); 1028 ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) { 1029 if (event.attributes.hasOwnProperty(key)) { 1030 event.attributes[key] = parseFloat(event.attributes[key]); 1031 } 1032 }); 1033 ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) { 1034 if (event.attributes.hasOwnProperty(key)) { 1035 event.attributes[key] = /YES/.test(event.attributes[key]); 1036 } 1037 }); 1038 this.trigger('data', event); 1039 return; 1040 } 1041 1042 match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine); 1043 1044 if (match && match[1]) { 1045 event = { 1046 type: 'tag', 1047 tagType: 'part-inf' 1048 }; 1049 event.attributes = parseAttributes$1(match[1]); 1050 ['PART-TARGET'].forEach(function (key) { 1051 if (event.attributes.hasOwnProperty(key)) { 1052 event.attributes[key] = parseFloat(event.attributes[key]); 1053 } 1054 }); 1055 this.trigger('data', event); 1056 return; 1057 } 1058 1059 match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine); 1060 1061 if (match && match[1]) { 1062 event = { 1063 type: 'tag', 1064 tagType: 'preload-hint' 1065 }; 1066 event.attributes = parseAttributes$1(match[1]); 1067 ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) { 1068 if (event.attributes.hasOwnProperty(key)) { 1069 event.attributes[key] = parseInt(event.attributes[key], 10); 1070 const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset'; 1071 event.attributes.byterange = event.attributes.byterange || {}; 1072 event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object. 1073 1074 delete event.attributes[key]; 1075 } 1076 }); 1077 this.trigger('data', event); 1078 return; 1079 } 1080 1081 match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine); 1082 1083 if (match && match[1]) { 1084 event = { 1085 type: 'tag', 1086 tagType: 'rendition-report' 1087 }; 1088 event.attributes = parseAttributes$1(match[1]); 1089 ['LAST-MSN', 'LAST-PART'].forEach(function (key) { 1090 if (event.attributes.hasOwnProperty(key)) { 1091 event.attributes[key] = parseInt(event.attributes[key], 10); 1092 } 1093 }); 1094 this.trigger('data', event); 1095 return; 1096 } 1097 1098 match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine); 1099 1100 if (match && match[1]) { 1101 event = { 1102 type: 'tag', 1103 tagType: 'daterange' 1104 }; 1105 event.attributes = parseAttributes$1(match[1]); 1106 ['ID', 'CLASS'].forEach(function (key) { 1107 if (event.attributes.hasOwnProperty(key)) { 1108 event.attributes[key] = String(event.attributes[key]); 1109 } 1110 }); 1111 ['START-DATE', 'END-DATE'].forEach(function (key) { 1112 if (event.attributes.hasOwnProperty(key)) { 1113 event.attributes[key] = new Date(event.attributes[key]); 1114 } 1115 }); 1116 ['DURATION', 'PLANNED-DURATION'].forEach(function (key) { 1117 if (event.attributes.hasOwnProperty(key)) { 1118 event.attributes[key] = parseFloat(event.attributes[key]); 1119 } 1120 }); 1121 ['END-ON-NEXT'].forEach(function (key) { 1122 if (event.attributes.hasOwnProperty(key)) { 1123 event.attributes[key] = /YES/i.test(event.attributes[key]); 1124 } 1125 }); 1126 ['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) { 1127 if (event.attributes.hasOwnProperty(key)) { 1128 event.attributes[key] = event.attributes[key].toString(16); 1129 } 1130 }); 1131 const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/; 1132 1133 for (const key in event.attributes) { 1134 if (!clientAttributePattern.test(key)) { 1135 continue; 1136 } 1137 1138 const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]); 1139 const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]); 1140 event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]); 1141 } 1142 1143 this.trigger('data', event); 1144 return; 1145 } 1146 1147 match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine); 1148 1149 if (match) { 1150 this.trigger('data', { 1151 type: 'tag', 1152 tagType: 'independent-segments' 1153 }); 1154 return; 1155 } 1156 1157 match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine); 1158 1159 if (match) { 1160 event = { 1161 type: 'tag', 1162 tagType: 'content-steering' 1163 }; 1164 event.attributes = parseAttributes$1(match[1]); 1165 this.trigger('data', event); 1166 return; 1167 } // unknown tag type 1168 1169 1170 this.trigger('data', { 1171 type: 'tag', 1172 data: newLine.slice(4) 1173 }); 1174 }); 1175 } 1176 /** 1177 * Add a parser for custom headers 1178 * 1179 * @param {Object} options a map of options for the added parser 1180 * @param {RegExp} options.expression a regular expression to match the custom header 1181 * @param {string} options.customType the custom type to register to the output 1182 * @param {Function} [options.dataParser] function to parse the line into an object 1183 * @param {boolean} [options.segment] should tag data be attached to the segment object 1184 */ 1185 1186 1187 addParser({ 1188 expression, 1189 customType, 1190 dataParser, 1191 segment 1192 }) { 1193 if (typeof dataParser !== 'function') { 1194 dataParser = line => line; 1195 } 1196 1197 this.customParsers.push(line => { 1198 const match = expression.exec(line); 1199 1200 if (match) { 1201 this.trigger('data', { 1202 type: 'custom', 1203 data: dataParser(line), 1204 customType, 1205 segment 1206 }); 1207 return true; 1208 } 1209 }); 1210 } 1211 /** 1212 * Add a custom header mapper 1213 * 1214 * @param {Object} options 1215 * @param {RegExp} options.expression a regular expression to match the custom header 1216 * @param {Function} options.map function to translate tag into a different tag 1217 */ 1218 1219 1220 addTagMapper({ 1221 expression, 1222 map 1223 }) { 1224 const mapFn = line => { 1225 if (expression.test(line)) { 1226 return map(line); 1227 } 1228 1229 return line; 1230 }; 1231 1232 this.tagMappers.push(mapFn); 1233 } 1234 1235 } 1236 1237 const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase()); 1238 1239 const camelCaseKeys = function (attributes) { 1240 const result = {}; 1241 Object.keys(attributes).forEach(function (key) { 1242 result[camelCase(key)] = attributes[key]; 1243 }); 1244 return result; 1245 }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration 1246 // we need this helper because defaults are based upon targetDuration and 1247 // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before 1248 // target durations are set. 1249 1250 1251 const setHoldBack = function (manifest) { 1252 const { 1253 serverControl, 1254 targetDuration, 1255 partTargetDuration 1256 } = manifest; 1257 1258 if (!serverControl) { 1259 return; 1260 } 1261 1262 const tag = '#EXT-X-SERVER-CONTROL'; 1263 const hb = 'holdBack'; 1264 const phb = 'partHoldBack'; 1265 const minTargetDuration = targetDuration && targetDuration * 3; 1266 const minPartDuration = partTargetDuration && partTargetDuration * 2; 1267 1268 if (targetDuration && !serverControl.hasOwnProperty(hb)) { 1269 serverControl[hb] = minTargetDuration; 1270 this.trigger('info', { 1271 message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).` 1272 }); 1273 } 1274 1275 if (minTargetDuration && serverControl[hb] < minTargetDuration) { 1276 this.trigger('warn', { 1277 message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})` 1278 }); 1279 serverControl[hb] = minTargetDuration; 1280 } // default no part hold back to part target duration * 3 1281 1282 1283 if (partTargetDuration && !serverControl.hasOwnProperty(phb)) { 1284 serverControl[phb] = partTargetDuration * 3; 1285 this.trigger('info', { 1286 message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).` 1287 }); 1288 } // if part hold back is too small default it to part target duration * 2 1289 1290 1291 if (partTargetDuration && serverControl[phb] < minPartDuration) { 1292 this.trigger('warn', { 1293 message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).` 1294 }); 1295 serverControl[phb] = minPartDuration; 1296 } 1297 }; 1298 /** 1299 * A parser for M3U8 files. The current interpretation of the input is 1300 * exposed as a property `manifest` on parser objects. It's just two lines to 1301 * create and parse a manifest once you have the contents available as a string: 1302 * 1303 * ```js 1304 * var parser = new m3u8.Parser(); 1305 * parser.push(xhr.responseText); 1306 * ``` 1307 * 1308 * New input can later be applied to update the manifest object by calling 1309 * `push` again. 1310 * 1311 * The parser attempts to create a usable manifest object even if the 1312 * underlying input is somewhat nonsensical. It emits `info` and `warning` 1313 * events during the parse if it encounters input that seems invalid or 1314 * requires some property of the manifest object to be defaulted. 1315 * 1316 * @class Parser 1317 * @extends Stream 1318 */ 1319 1320 1321 class Parser extends Stream { 1322 constructor() { 1323 super(); 1324 this.lineStream = new LineStream(); 1325 this.parseStream = new ParseStream(); 1326 this.lineStream.pipe(this.parseStream); 1327 this.lastProgramDateTime = null; 1328 /* eslint-disable consistent-this */ 1329 1330 const self = this; 1331 /* eslint-enable consistent-this */ 1332 1333 const uris = []; 1334 let currentUri = {}; // if specified, the active EXT-X-MAP definition 1335 1336 let currentMap; // if specified, the active decryption key 1337 1338 let key; 1339 let hasParts = false; 1340 1341 const noop = function () {}; 1342 1343 const defaultMediaGroups = { 1344 'AUDIO': {}, 1345 'VIDEO': {}, 1346 'CLOSED-CAPTIONS': {}, 1347 'SUBTITLES': {} 1348 }; // This is the Widevine UUID from DASH IF IOP. The same exact string is 1349 // used in MPDs with Widevine encrypted streams. 1350 1351 const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities 1352 1353 let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data 1354 1355 this.manifest = { 1356 allowCache: true, 1357 discontinuityStarts: [], 1358 dateRanges: [], 1359 segments: [] 1360 }; // keep track of the last seen segment's byte range end, as segments are not required 1361 // to provide the offset, in which case it defaults to the next byte after the 1362 // previous segment 1363 1364 let lastByterangeEnd = 0; // keep track of the last seen part's byte range end. 1365 1366 let lastPartByterangeEnd = 0; 1367 const dateRangeTags = {}; 1368 this.on('end', () => { 1369 // only add preloadSegment if we don't yet have a uri for it. 1370 // and we actually have parts/preloadHints 1371 if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) { 1372 return; 1373 } 1374 1375 if (!currentUri.map && currentMap) { 1376 currentUri.map = currentMap; 1377 } 1378 1379 if (!currentUri.key && key) { 1380 currentUri.key = key; 1381 } 1382 1383 if (!currentUri.timeline && typeof currentTimeline === 'number') { 1384 currentUri.timeline = currentTimeline; 1385 } 1386 1387 this.manifest.preloadSegment = currentUri; 1388 }); // update the manifest with the m3u8 entry from the parse stream 1389 1390 this.parseStream.on('data', function (entry) { 1391 let mediaGroup; 1392 let rendition;
vendor: 22,925 bytes, lines 1392-1965
1392 1393 ({ 1394 tag() { 1395 // switch based on the tag type 1396 (({ 1397 version() { 1398 if (entry.version) { 1399 this.manifest.version = entry.version; 1400 } 1401 }, 1402 1403 'allow-cache'() { 1404 this.manifest.allowCache = entry.allowed; 1405 1406 if (!('allowed' in entry)) { 1407 this.trigger('info', { 1408 message: 'defaulting allowCache to YES' 1409 }); 1410 this.manifest.allowCache = true; 1411 } 1412 }, 1413 1414 byterange() { 1415 const byterange = {}; 1416 1417 if ('length' in entry) { 1418 currentUri.byterange = byterange; 1419 byterange.length = entry.length; 1420 1421 if (!('offset' in entry)) { 1422 /* 1423 * From the latest spec (as of this writing): 1424 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2 1425 * 1426 * Same text since EXT-X-BYTERANGE's introduction in draft 7: 1427 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1) 1428 * 1429 * "If o [offset] is not present, the sub-range begins at the next byte 1430 * following the sub-range of the previous media segment." 1431 */ 1432 entry.offset = lastByterangeEnd; 1433 } 1434 } 1435 1436 if ('offset' in entry) { 1437 currentUri.byterange = byterange; 1438 byterange.offset = entry.offset; 1439 } 1440 1441 lastByterangeEnd = byterange.offset + byterange.length; 1442 }, 1443 1444 endlist() { 1445 this.manifest.endList = true; 1446 }, 1447 1448 inf() { 1449 if (!('mediaSequence' in this.manifest)) { 1450 this.manifest.mediaSequence = 0; 1451 this.trigger('info', { 1452 message: 'defaulting media sequence to zero' 1453 }); 1454 } 1455 1456 if (!('discontinuitySequence' in this.manifest)) { 1457 this.manifest.discontinuitySequence = 0; 1458 this.trigger('info', { 1459 message: 'defaulting discontinuity sequence to zero' 1460 }); 1461 } 1462 1463 if (entry.title) { 1464 currentUri.title = entry.title; 1465 } 1466 1467 if (entry.duration > 0) { 1468 currentUri.duration = entry.duration; 1469 } 1470 1471 if (entry.duration === 0) { 1472 currentUri.duration = 0.01; 1473 this.trigger('info', { 1474 message: 'updating zero segment duration to a small value' 1475 }); 1476 } 1477 1478 this.manifest.segments = uris; 1479 }, 1480 1481 key() { 1482 if (!entry.attributes) { 1483 this.trigger('warn', { 1484 message: 'ignoring key declaration without attribute list' 1485 }); 1486 return; 1487 } // clear the active encryption key 1488 1489 1490 if (entry.attributes.METHOD === 'NONE') { 1491 key = null; 1492 return; 1493 } 1494 1495 if (!entry.attributes.URI) { 1496 this.trigger('warn', { 1497 message: 'ignoring key declaration without URI' 1498 }); 1499 return; 1500 } 1501 1502 if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') { 1503 this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this. 1504 1505 this.manifest.contentProtection['com.apple.fps.1_0'] = { 1506 attributes: entry.attributes 1507 }; 1508 return; 1509 } 1510 1511 if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') { 1512 this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this. 1513 1514 this.manifest.contentProtection['com.microsoft.playready'] = { 1515 uri: entry.attributes.URI 1516 }; 1517 return; 1518 } // check if the content is encrypted for Widevine 1519 // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf 1520 1521 1522 if (entry.attributes.KEYFORMAT === widevineUuid) { 1523 const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC']; 1524 1525 if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) { 1526 this.trigger('warn', { 1527 message: 'invalid key method provided for Widevine' 1528 }); 1529 return; 1530 } 1531 1532 if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') { 1533 this.trigger('warn', { 1534 message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead' 1535 }); 1536 } 1537 1538 if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') { 1539 this.trigger('warn', { 1540 message: 'invalid key URI provided for Widevine' 1541 }); 1542 return; 1543 } 1544 1545 if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) { 1546 this.trigger('warn', { 1547 message: 'invalid key ID provided for Widevine' 1548 }); 1549 return; 1550 } // if Widevine key attributes are valid, store them as `contentProtection` 1551 // on the manifest to emulate Widevine tag structure in a DASH mpd 1552 1553 1554 this.manifest.contentProtection = this.manifest.contentProtection || {}; 1555 this.manifest.contentProtection['com.widevine.alpha'] = { 1556 attributes: { 1557 schemeIdUri: entry.attributes.KEYFORMAT, 1558 // remove '0x' from the key id string 1559 keyId: entry.attributes.KEYID.substring(2) 1560 }, 1561 // decode the base64-encoded PSSH box 1562 pssh: decodeB64ToUint8Array(entry.attributes.URI.split(',')[1]) 1563 }; 1564 return; 1565 } 1566 1567 if (!entry.attributes.METHOD) { 1568 this.trigger('warn', { 1569 message: 'defaulting key method to AES-128' 1570 }); 1571 } // setup an encryption key for upcoming segments 1572 1573 1574 key = { 1575 method: entry.attributes.METHOD || 'AES-128', 1576 uri: entry.attributes.URI 1577 }; 1578 1579 if (typeof entry.attributes.IV !== 'undefined') { 1580 key.iv = entry.attributes.IV; 1581 } 1582 }, 1583 1584 'media-sequence'() { 1585 if (!isFinite(entry.number)) { 1586 this.trigger('warn', { 1587 message: 'ignoring invalid media sequence: ' + entry.number 1588 }); 1589 return; 1590 } 1591 1592 this.manifest.mediaSequence = entry.number; 1593 }, 1594 1595 'discontinuity-sequence'() { 1596 if (!isFinite(entry.number)) { 1597 this.trigger('warn', { 1598 message: 'ignoring invalid discontinuity sequence: ' + entry.number 1599 }); 1600 return; 1601 } 1602 1603 this.manifest.discontinuitySequence = entry.number; 1604 currentTimeline = entry.number; 1605 }, 1606 1607 'playlist-type'() { 1608 if (!/VOD|EVENT/.test(entry.playlistType)) { 1609 this.trigger('warn', { 1610 message: 'ignoring unknown playlist type: ' + entry.playlist 1611 }); 1612 return; 1613 } 1614 1615 this.manifest.playlistType = entry.playlistType; 1616 }, 1617 1618 map() { 1619 currentMap = {}; 1620 1621 if (entry.uri) { 1622 currentMap.uri = entry.uri; 1623 } 1624 1625 if (entry.byterange) { 1626 currentMap.byterange = entry.byterange; 1627 } 1628 1629 if (key) { 1630 currentMap.key = key; 1631 } 1632 }, 1633 1634 'stream-inf'() { 1635 this.manifest.playlists = uris; 1636 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 1637 1638 if (!entry.attributes) { 1639 this.trigger('warn', { 1640 message: 'ignoring empty stream-inf attributes' 1641 }); 1642 return; 1643 } 1644 1645 if (!currentUri.attributes) { 1646 currentUri.attributes = {}; 1647 } 1648 1649 _extends(currentUri.attributes, entry.attributes); 1650 }, 1651 1652 media() { 1653 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 1654 1655 if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { 1656 this.trigger('warn', { 1657 message: 'ignoring incomplete or missing media group' 1658 }); 1659 return; 1660 } // find the media group, creating defaults as necessary 1661 1662 1663 const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; 1664 mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; 1665 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata 1666 1667 rendition = { 1668 default: /yes/i.test(entry.attributes.DEFAULT) 1669 }; 1670 1671 if (rendition.default) { 1672 rendition.autoselect = true; 1673 } else { 1674 rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); 1675 } 1676 1677 if (entry.attributes.LANGUAGE) { 1678 rendition.language = entry.attributes.LANGUAGE; 1679 } 1680 1681 if (entry.attributes.URI) { 1682 rendition.uri = entry.attributes.URI; 1683 } 1684 1685 if (entry.attributes['INSTREAM-ID']) { 1686 rendition.instreamId = entry.attributes['INSTREAM-ID']; 1687 } 1688 1689 if (entry.attributes.CHARACTERISTICS) { 1690 rendition.characteristics = entry.attributes.CHARACTERISTICS; 1691 } 1692 1693 if (entry.attributes.FORCED) { 1694 rendition.forced = /yes/i.test(entry.attributes.FORCED); 1695 } // insert the new rendition 1696 1697 1698 mediaGroup[entry.attributes.NAME] = rendition; 1699 }, 1700 1701 discontinuity() { 1702 currentTimeline += 1; 1703 currentUri.discontinuity = true; 1704 this.manifest.discontinuityStarts.push(uris.length); 1705 }, 1706 1707 'program-date-time'() { 1708 if (typeof this.manifest.dateTimeString === 'undefined') { 1709 // PROGRAM-DATE-TIME is a media-segment tag, but for backwards 1710 // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag 1711 // to the manifest object 1712 // TODO: Consider removing this in future major version 1713 this.manifest.dateTimeString = entry.dateTimeString; 1714 this.manifest.dateTimeObject = entry.dateTimeObject; 1715 } 1716 1717 currentUri.dateTimeString = entry.dateTimeString; 1718 currentUri.dateTimeObject = entry.dateTimeObject; 1719 const { 1720 lastProgramDateTime 1721 } = this; 1722 this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence. 1723 // Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference. 1724 1725 if (lastProgramDateTime === null) { 1726 // Extrapolate Program Date Time backward 1727 // Since it is first program date time occurrence we're assuming that 1728 // all this.manifest.segments have no program date time info 1729 this.manifest.segments.reduceRight((programDateTime, segment) => { 1730 segment.programDateTime = programDateTime - segment.duration * 1000; 1731 return segment.programDateTime; 1732 }, this.lastProgramDateTime); 1733 } 1734 }, 1735 1736 targetduration() { 1737 if (!isFinite(entry.duration) || entry.duration < 0) { 1738 this.trigger('warn', { 1739 message: 'ignoring invalid target duration: ' + entry.duration 1740 }); 1741 return; 1742 } 1743 1744 this.manifest.targetDuration = entry.duration; 1745 setHoldBack.call(this, this.manifest); 1746 }, 1747 1748 start() { 1749 if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { 1750 this.trigger('warn', { 1751 message: 'ignoring start declaration without appropriate attribute list' 1752 }); 1753 return; 1754 } 1755 1756 this.manifest.start = { 1757 timeOffset: entry.attributes['TIME-OFFSET'], 1758 precise: entry.attributes.PRECISE 1759 }; 1760 }, 1761 1762 'cue-out'() { 1763 currentUri.cueOut = entry.data; 1764 }, 1765 1766 'cue-out-cont'() { 1767 currentUri.cueOutCont = entry.data; 1768 }, 1769 1770 'cue-in'() { 1771 currentUri.cueIn = entry.data; 1772 }, 1773 1774 'skip'() { 1775 this.manifest.skip = camelCaseKeys(entry.attributes); 1776 this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']); 1777 }, 1778 1779 'part'() { 1780 hasParts = true; // parts are always specifed before a segment 1781 1782 const segmentIndex = this.manifest.segments.length; 1783 const part = camelCaseKeys(entry.attributes); 1784 currentUri.parts = currentUri.parts || []; 1785 currentUri.parts.push(part); 1786 1787 if (part.byterange) { 1788 if (!part.byterange.hasOwnProperty('offset')) { 1789 part.byterange.offset = lastPartByterangeEnd; 1790 } 1791 1792 lastPartByterangeEnd = part.byterange.offset + part.byterange.length; 1793 } 1794 1795 const partIndex = currentUri.parts.length - 1; 1796 this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']); 1797 1798 if (this.manifest.renditionReports) { 1799 this.manifest.renditionReports.forEach((r, i) => { 1800 if (!r.hasOwnProperty('lastPart')) { 1801 this.trigger('warn', { 1802 message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART` 1803 }); 1804 } 1805 }); 1806 } 1807 }, 1808 1809 'server-control'() { 1810 const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes); 1811 1812 if (!attrs.hasOwnProperty('canBlockReload')) { 1813 attrs.canBlockReload = false; 1814 this.trigger('info', { 1815 message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false' 1816 }); 1817 } 1818 1819 setHoldBack.call(this, this.manifest); 1820 1821 if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) { 1822 this.trigger('warn', { 1823 message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set' 1824 }); 1825 } 1826 }, 1827 1828 'preload-hint'() { 1829 // parts are always specifed before a segment 1830 const segmentIndex = this.manifest.segments.length; 1831 const hint = camelCaseKeys(entry.attributes); 1832 const isPart = hint.type && hint.type === 'PART'; 1833 currentUri.preloadHints = currentUri.preloadHints || []; 1834 currentUri.preloadHints.push(hint); 1835 1836 if (hint.byterange) { 1837 if (!hint.byterange.hasOwnProperty('offset')) { 1838 // use last part byterange end or zero if not a part. 1839 hint.byterange.offset = isPart ? lastPartByterangeEnd : 0; 1840 1841 if (isPart) { 1842 lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length; 1843 } 1844 } 1845 } 1846 1847 const index = currentUri.preloadHints.length - 1; 1848 this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']); 1849 1850 if (!hint.type) { 1851 return; 1852 } // search through all preload hints except for the current one for 1853 // a duplicate type. 1854 1855 1856 for (let i = 0; i < currentUri.preloadHints.length - 1; i++) { 1857 const otherHint = currentUri.preloadHints[i]; 1858 1859 if (!otherHint.type) { 1860 continue; 1861 } 1862 1863 if (otherHint.type === hint.type) { 1864 this.trigger('warn', { 1865 message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}` 1866 }); 1867 } 1868 } 1869 }, 1870 1871 'rendition-report'() { 1872 const report = camelCaseKeys(entry.attributes); 1873 this.manifest.renditionReports = this.manifest.renditionReports || []; 1874 this.manifest.renditionReports.push(report); 1875 const index = this.manifest.renditionReports.length - 1; 1876 const required = ['LAST-MSN', 'URI']; 1877 1878 if (hasParts) { 1879 required.push('LAST-PART'); 1880 } 1881 1882 this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required); 1883 }, 1884 1885 'part-inf'() { 1886 this.manifest.partInf = camelCaseKeys(entry.attributes); 1887 this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']); 1888 1889 if (this.manifest.partInf.partTarget) { 1890 this.manifest.partTargetDuration = this.manifest.partInf.partTarget; 1891 } 1892 1893 setHoldBack.call(this, this.manifest); 1894 }, 1895 1896 'daterange'() { 1897 this.manifest.dateRanges.push(camelCaseKeys(entry.attributes)); 1898 const index = this.manifest.dateRanges.length - 1; 1899 this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']); 1900 const dateRange = this.manifest.dateRanges[index]; 1901 1902 if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) { 1903 this.trigger('warn', { 1904 message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE' 1905 }); 1906 } 1907 1908 if (dateRange.duration && dateRange.duration < 0) { 1909 this.trigger('warn', { 1910 message: 'EXT-X-DATERANGE DURATION must not be negative' 1911 }); 1912 } 1913 1914 if (dateRange.plannedDuration && dateRange.plannedDuration < 0) { 1915 this.trigger('warn', { 1916 message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative' 1917 }); 1918 } 1919 1920 const endOnNextYes = !!dateRange.endOnNext; 1921 1922 if (endOnNextYes && !dateRange.class) { 1923 this.trigger('warn', { 1924 message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute' 1925 }); 1926 } 1927 1928 if (endOnNextYes && (dateRange.duration || dateRange.endDate)) { 1929 this.trigger('warn', { 1930 message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes' 1931 }); 1932 } 1933 1934 if (dateRange.duration && dateRange.endDate) { 1935 const startDate = dateRange.startDate; 1936 const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000; 1937 this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds); 1938 } 1939 1940 if (!dateRangeTags[dateRange.id]) { 1941 dateRangeTags[dateRange.id] = dateRange; 1942 } else { 1943 for (const attribute in dateRangeTags[dateRange.id]) { 1944 if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) { 1945 this.trigger('warn', { 1946 message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values' 1947 }); 1948 break; 1949 } 1950 } // if tags with the same ID do not have conflicting attributes, merge them 1951 1952 1953 const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id); 1954 this.manifest.dateRanges[dateRangeWithSameId] = _extends(this.manifest.dateRanges[dateRangeWithSameId], dateRange); 1955 dateRangeTags[dateRange.id] = _extends(dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last 1956 1957 this.manifest.dateRanges.pop(); 1958 } 1959 }, 1960 1961 'independent-segments'() { 1962 this.manifest.independentSegments = true; 1963 }, 1964 1965 '
vendor: 6,792 bytes, lines 1965-2169
1965content-steering'() { 1966 this.manifest.contentSteering = camelCaseKeys(entry.attributes); 1967 this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']); 1968 } 1969 1970 })[entry.tagType] || noop).call(self); 1971 }, 1972 1973 uri() { 1974 currentUri.uri = entry.uri; 1975 uris.push(currentUri); // if no explicit duration was declared, use the target duration 1976 1977 if (this.manifest.targetDuration && !('duration' in currentUri)) { 1978 this.trigger('warn', { 1979 message: 'defaulting segment duration to the target duration' 1980 }); 1981 currentUri.duration = this.manifest.targetDuration; 1982 } // annotate with encryption information, if necessary 1983 1984 1985 if (key) { 1986 currentUri.key = key; 1987 } 1988 1989 currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary 1990 1991 if (currentMap) { 1992 currentUri.map = currentMap; 1993 } // reset the last byterange end as it needs to be 0 between parts 1994 1995 1996 lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward 1997 1998 if (this.lastProgramDateTime !== null) { 1999 currentUri.programDateTime = this.lastProgramDateTime; 2000 this.lastProgramDateTime += currentUri.duration * 1000; 2001 } // prepare for the next URI 2002 2003 2004 currentUri = {}; 2005 }, 2006 2007 comment() {// comments are not important for playback 2008 }, 2009 2010 custom() { 2011 // if this is segment-level data attach the output to the segment 2012 if (entry.segment) { 2013 currentUri.custom = currentUri.custom || {}; 2014 currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object 2015 } else { 2016 this.manifest.custom = this.manifest.custom || {}; 2017 this.manifest.custom[entry.customType] = entry.data; 2018 } 2019 } 2020 2021 })[entry.type].call(self); 2022 }); 2023 } 2024 2025 warnOnMissingAttributes_(identifier, attributes, required) { 2026 const missing = []; 2027 required.forEach(function (key) { 2028 if (!attributes.hasOwnProperty(key)) { 2029 missing.push(key); 2030 } 2031 }); 2032 2033 if (missing.length) { 2034 this.trigger('warn', { 2035 message: `${identifier} lacks required attribute(s): ${missing.join(', ')}` 2036 }); 2037 } 2038 } 2039 /** 2040 * Parse the input string and update the manifest object. 2041 * 2042 * @param {string} chunk a potentially incomplete portion of the manifest 2043 */ 2044 2045 2046 push(chunk) { 2047 this.lineStream.push(chunk); 2048 } 2049 /** 2050 * Flush any remaining input. This can be handy if the last line of an M3U8 2051 * manifest did not contain a trailing newline but the file has been 2052 * completely received. 2053 */ 2054 2055 2056 end() { 2057 // flush any buffered input 2058 this.lineStream.push('\n'); 2059 2060 if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) { 2061 this.trigger('warn', { 2062 message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag' 2063 }); 2064 } 2065 2066 this.lastProgramDateTime = null; 2067 this.trigger('end'); 2068 } 2069 /** 2070 * Add an additional parser for non-standard tags 2071 * 2072 * @param {Object} options a map of options for the added parser 2073 * @param {RegExp} options.expression a regular expression to match the custom header 2074 * @param {string} options.customType the custom type to register to the output 2075 * @param {Function} [options.dataParser] function to parse the line into an object 2076 * @param {boolean} [options.segment] should tag data be attached to the segment object 2077 */ 2078 2079 2080 addParser(options) { 2081 this.parseStream.addParser(options); 2082 } 2083 /** 2084 * Add a custom header mapper 2085 * 2086 * @param {Object} options 2087 * @param {RegExp} options.expression a regular expression to match the custom header 2088 * @param {Function} options.map function to translate tag into a different tag 2089 */ 2090 2091 2092 addTagMapper(options) { 2093 this.parseStream.addTagMapper(options); 2094 } 2095 2096 } 2097 2098 var regexs = { 2099 // to determine mime types 2100 mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v|stpp.ttml.im1t)/, 2101 webm: /^(vp0?[89]|av0?1|opus|vorbis)/, 2102 ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/, 2103 // to determine if a codec is audio or video 2104 video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/, 2105 audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3|speex|aac)/, 2106 text: /^(stpp.ttml.im1t)/, 2107 // mux.js support regex 2108 muxerVideo: /^(avc0?1)/, 2109 muxerAudio: /^(mp4a)/, 2110 // match nothing as muxer does not support text right now. 2111 // there cannot never be a character before the start of a string 2112 // so this matches nothing. 2113 muxerText: /a^/ 2114 }; 2115 var mediaTypes = ['video', 'audio', 'text']; 2116 var upperMediaTypes = ['Video', 'Audio', 'Text']; 2117 /** 2118 * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard 2119 * `avc1.<hhhhhh>` 2120 * 2121 * @param {string} codec 2122 * Codec string to translate 2123 * @return {string} 2124 * The translated codec string 2125 */ 2126 2127 var translateLegacyCodec = function translateLegacyCodec(codec) { 2128 if (!codec) { 2129 return codec; 2130 } 2131 2132 return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { 2133 var profileHex = ('00' + Number(profile).toString(16)).slice(-2); 2134 var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); 2135 return 'avc1.' + profileHex + '00' + avcLevelHex; 2136 }); 2137 }; 2138 /** 2139 * @typedef {Object} ParsedCodecInfo 2140 * @property {number} codecCount 2141 * Number of codecs parsed 2142 * @property {string} [videoCodec] 2143 * Parsed video codec (if found) 2144 * @property {string} [videoObjectTypeIndicator] 2145 * Video object type indicator (if found) 2146 * @property {string|null} audioProfile 2147 * Audio profile 2148 */ 2149 2150 /** 2151 * Parses a codec string to retrieve the number of codecs specified, the video codec and 2152 * object type indicator, and the audio profile. 2153 * 2154 * @param {string} [codecString] 2155 * The codec string to parse 2156 * @return {ParsedCodecInfo} 2157 * Parsed codec info 2158 */ 2159 2160 var parseCodecs = function parseCodecs(codecString) { 2161 if (codecString === void 0) { 2162 codecString = ''; 2163 } 2164 2165 var codecs = codecString.split(','); 2166 var result = []; 2167 codecs.forEach(function (codec) { 2168 codec = codec.trim(); 2169 var codecType;
vendor: 4,372 bytes, lines 2170-2324
2170 mediaTypes.forEach(function (name) { 2171 var match = regexs[name].exec(codec.toLowerCase()); 2172 2173 if (!match || match.length <= 1) { 2174 return; 2175 } 2176 2177 codecType = name; // maintain codec case 2178 2179 var type = codec.substring(0, match[1].length); 2180 var details = codec.replace(type, ''); 2181 result.push({ 2182 type: type, 2183 details: details, 2184 mediaType: name 2185 }); 2186 }); 2187 2188 if (!codecType) { 2189 result.push({ 2190 type: codec, 2191 details: '', 2192 mediaType: 'unknown' 2193 }); 2194 } 2195 }); 2196 return result; 2197 }; 2198 /** 2199 * Returns a ParsedCodecInfo object for the default alternate audio playlist if there is 2200 * a default alternate audio playlist for the provided audio group. 2201 * 2202 * @param {Object} master 2203 * The master playlist 2204 * @param {string} audioGroupId 2205 * ID of the audio group for which to find the default codec info 2206 * @return {ParsedCodecInfo} 2207 * Parsed codec info 2208 */ 2209 2210 var codecsFromDefault = function codecsFromDefault(master, audioGroupId) { 2211 if (!master.mediaGroups.AUDIO || !audioGroupId) { 2212 return null; 2213 } 2214 2215 var audioGroup = master.mediaGroups.AUDIO[audioGroupId]; 2216 2217 if (!audioGroup) { 2218 return null; 2219 } 2220 2221 for (var name in audioGroup) { 2222 var audioType = audioGroup[name]; 2223 2224 if (audioType.default && audioType.playlists) { 2225 // codec should be the same for all playlists within the audio type 2226 return parseCodecs(audioType.playlists[0].attributes.CODECS); 2227 } 2228 } 2229 2230 return null; 2231 }; 2232 var isAudioCodec = function isAudioCodec(codec) { 2233 if (codec === void 0) { 2234 codec = ''; 2235 } 2236 2237 return regexs.audio.test(codec.trim().toLowerCase()); 2238 }; 2239 var isTextCodec = function isTextCodec(codec) { 2240 if (codec === void 0) { 2241 codec = ''; 2242 } 2243 2244 return regexs.text.test(codec.trim().toLowerCase()); 2245 }; 2246 var getMimeForCodec = function getMimeForCodec(codecString) { 2247 if (!codecString || typeof codecString !== 'string') { 2248 return; 2249 } 2250 2251 var codecs = codecString.toLowerCase().split(',').map(function (c) { 2252 return translateLegacyCodec(c.trim()); 2253 }); // default to video type 2254 2255 var type = 'video'; // only change to audio type if the only codec we have is 2256 // audio 2257 2258 if (codecs.length === 1 && isAudioCodec(codecs[0])) { 2259 type = 'audio'; 2260 } else if (codecs.length === 1 && isTextCodec(codecs[0])) { 2261 // text uses application/<container> for now 2262 type = 'application'; 2263 } // default the container to mp4 2264 2265 2266 var container = 'mp4'; // every codec must be able to go into the container 2267 // for that container to be the correct one 2268 2269 if (codecs.every(function (c) { 2270 return regexs.mp4.test(c); 2271 })) { 2272 container = 'mp4'; 2273 } else if (codecs.every(function (c) { 2274 return regexs.webm.test(c); 2275 })) { 2276 container = 'webm'; 2277 } else if (codecs.every(function (c) { 2278 return regexs.ogg.test(c); 2279 })) { 2280 container = 'ogg'; 2281 } 2282 2283 return type + "/" + container + ";codecs=\"" + codecString + "\""; 2284 }; 2285 var browserSupportsCodec = function browserSupportsCodec(codecString) { 2286 if (codecString === void 0) { 2287 codecString = ''; 2288 } 2289 2290 return window.MediaSource && window.MediaSource.isTypeSupported && window.MediaSource.isTypeSupported(getMimeForCodec(codecString)) || false; 2291 }; 2292 var muxerSupportsCodec = function muxerSupportsCodec(codecString) { 2293 if (codecString === void 0) { 2294 codecString = ''; 2295 } 2296 2297 return codecString.toLowerCase().split(',').every(function (codec) { 2298 codec = codec.trim(); // any match is supported. 2299 2300 for (var i = 0; i < upperMediaTypes.length; i++) { 2301 var type = upperMediaTypes[i]; 2302 2303 if (regexs["muxer" + type].test(codec)) { 2304 return true; 2305 } 2306 } 2307 2308 return false; 2309 }); 2310 }; 2311 var DEFAULT_AUDIO_CODEC = 'mp4a.40.2'; 2312 var DEFAULT_VIDEO_CODEC = 'avc1.4d400d'; 2313 2314 /** 2315 * Provides a compatibility layer between Video.js 7 and 8 API changes for VHS. 2316 */ 2317 /** 2318 * Delegates to videojs.obj.merge (Video.js 8) or 2319 * videojs.mergeOptions (Video.js 7). 2320 */ 2321 2322 function merge$1(...args) { 2323 const context = videojs__default["default"].obj || videojs__default["default"]; 2324 const fn = context.merge || context.mergeOptions;
2325 return fn.apply(context, args); 2326 } 2327 /** 2328 * Delegates to videojs.time.createTimeRanges (Video.js 8) or 2329 * videojs.createTimeRanges (Video.js 7). 2330 */ 2331 2332 function createTimeRanges(...args) { 2333 const context = videojs__default["default"].time || videojs__default["default"]; 2334 const fn = context.createTimeRanges || context.createTimeRanges; 2335 return fn.apply(context, args)
vendor: 47,821 bytes, lines 2335-3897
2335; 2336 } 2337 2338 /** 2339 * ranges 2340 * 2341 * Utilities for working with TimeRanges. 2342 * 2343 */ 2344 2345 const TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range 2346 // can be misleading because of precision differences or when the current media has poorly 2347 // aligned audio and video, which can cause values to be slightly off from what you would 2348 // expect. This value is what we consider to be safe to use in such comparisons to account 2349 // for these scenarios. 2350 2351 const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 2352 2353 const filterRanges = function (timeRanges, predicate) { 2354 const results = []; 2355 let i; 2356 2357 if (timeRanges && timeRanges.length) { 2358 // Search for ranges that match the predicate 2359 for (i = 0; i < timeRanges.length; i++) { 2360 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 2361 results.push([timeRanges.start(i), timeRanges.end(i)]); 2362 } 2363 } 2364 } 2365 2366 return createTimeRanges(results); 2367 }; 2368 /** 2369 * Attempts to find the buffered TimeRange that contains the specified 2370 * time. 2371 * 2372 * @param {TimeRanges} buffered - the TimeRanges object to query 2373 * @param {number} time - the time to filter on. 2374 * @return {TimeRanges} a new TimeRanges object 2375 */ 2376 2377 2378 const findRange = function (buffered, time) { 2379 return filterRanges(buffered, function (start, end) { 2380 return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time; 2381 }); 2382 }; 2383 /** 2384 * Returns the TimeRanges that begin later than the specified time. 2385 * 2386 * @param {TimeRanges} timeRanges - the TimeRanges object to query 2387 * @param {number} time - the time to filter on. 2388 * @return {TimeRanges} a new TimeRanges object. 2389 */ 2390 2391 const findNextRange = function (timeRanges, time) { 2392 return filterRanges(timeRanges, function (start) { 2393 return start - TIME_FUDGE_FACTOR >= time; 2394 }); 2395 }; 2396 /** 2397 * Returns gaps within a list of TimeRanges 2398 * 2399 * @param {TimeRanges} buffered - the TimeRanges object 2400 * @return {TimeRanges} a TimeRanges object of gaps 2401 */ 2402 2403 const findGaps = function (buffered) { 2404 if (buffered.length < 2) { 2405 return createTimeRanges(); 2406 } 2407 2408 const ranges = []; 2409 2410 for (let i = 1; i < buffered.length; i++) { 2411 const start = buffered.end(i - 1); 2412 const end = buffered.start(i); 2413 ranges.push([start, end]); 2414 } 2415 2416 return createTimeRanges(ranges); 2417 }; 2418 /** 2419 * Calculate the intersection of two TimeRanges 2420 * 2421 * @param {TimeRanges} bufferA 2422 * @param {TimeRanges} bufferB 2423 * @return {TimeRanges} The interesection of `bufferA` with `bufferB` 2424 */ 2425 2426 const bufferIntersection = function (bufferA, bufferB) { 2427 let start = null; 2428 let end = null; 2429 let arity = 0; 2430 const extents = []; 2431 const ranges = []; 2432 2433 if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) { 2434 return createTimeRanges(); 2435 } // Handle the case where we have both buffers and create an 2436 // intersection of the two 2437 2438 2439 let count = bufferA.length; // A) Gather up all start and end times 2440 2441 while (count--) { 2442 extents.push({ 2443 time: bufferA.start(count), 2444 type: 'start' 2445 }); 2446 extents.push({ 2447 time: bufferA.end(count), 2448 type: 'end' 2449 }); 2450 } 2451 2452 count = bufferB.length; 2453 2454 while (count--) { 2455 extents.push({ 2456 time: bufferB.start(count), 2457 type: 'start' 2458 }); 2459 extents.push({ 2460 time: bufferB.end(count), 2461 type: 'end' 2462 }); 2463 } // B) Sort them by time 2464 2465 2466 extents.sort(function (a, b) { 2467 return a.time - b.time; 2468 }); // C) Go along one by one incrementing arity for start and decrementing 2469 // arity for ends 2470 2471 for (count = 0; count < extents.length; count++) { 2472 if (extents[count].type === 'start') { 2473 arity++; // D) If arity is ever incremented to 2 we are entering an 2474 // overlapping range 2475 2476 if (arity === 2) { 2477 start = extents[count].time; 2478 } 2479 } else if (extents[count].type === 'end') { 2480 arity--; // E) If arity is ever decremented to 1 we leaving an 2481 // overlapping range 2482 2483 if (arity === 1) { 2484 end = extents[count].time; 2485 } 2486 } // F) Record overlapping ranges 2487 2488 2489 if (start !== null && end !== null) { 2490 ranges.push([start, end]); 2491 start = null; 2492 end = null; 2493 } 2494 } 2495 2496 return createTimeRanges(ranges); 2497 }; 2498 /** 2499 * Gets a human readable string for a TimeRange 2500 * 2501 * @param {TimeRange} range 2502 * @return {string} a human readable string 2503 */ 2504 2505 const printableRange = range => { 2506 const strArr = []; 2507 2508 if (!range || !range.length) { 2509 return ''; 2510 } 2511 2512 for (let i = 0; i < range.length; i++) { 2513 strArr.push(range.start(i) + ' => ' + range.end(i)); 2514 } 2515 2516 return strArr.join(', '); 2517 }; 2518 /** 2519 * Calculates the amount of time left in seconds until the player hits the end of the 2520 * buffer and causes a rebuffer 2521 * 2522 * @param {TimeRange} buffered 2523 * The state of the buffer 2524 * @param {Numnber} currentTime 2525 * The current time of the player 2526 * @param {number} playbackRate 2527 * The current playback rate of the player. Defaults to 1. 2528 * @return {number} 2529 * Time until the player has to start rebuffering in seconds. 2530 * @function timeUntilRebuffer 2531 */ 2532 2533 const timeUntilRebuffer = function (buffered, currentTime, playbackRate = 1) { 2534 const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 2535 return (bufferedEnd - currentTime) / playbackRate; 2536 }; 2537 /** 2538 * Converts a TimeRanges object into an array representation 2539 * 2540 * @param {TimeRanges} timeRanges 2541 * @return {Array} 2542 */ 2543 2544 const timeRangesToArray = timeRanges => { 2545 const timeRangesList = []; 2546 2547 for (let i = 0; i < timeRanges.length; i++) { 2548 timeRangesList.push({ 2549 start: timeRanges.start(i), 2550 end: timeRanges.end(i) 2551 }); 2552 } 2553 2554 return timeRangesList; 2555 }; 2556 /** 2557 * Determines if two time range objects are different. 2558 * 2559 * @param {TimeRange} a 2560 * the first time range object to check 2561 * 2562 * @param {TimeRange} b 2563 * the second time range object to check 2564 * 2565 * @return {Boolean} 2566 * Whether the time range objects differ 2567 */ 2568 2569 const isRangeDifferent = function (a, b) { 2570 // same object 2571 if (a === b) { 2572 return false; 2573 } // one or the other is undefined 2574 2575 2576 if (!a && b || !b && a) { 2577 return true; 2578 } // length is different 2579 2580 2581 if (a.length !== b.length) { 2582 return true; 2583 } // see if any start/end pair is different 2584 2585 2586 for (let i = 0; i < a.length; i++) { 2587 if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) { 2588 return true; 2589 } 2590 } // if the length and every pair is the same 2591 // this is the same time range 2592 2593 2594 return false; 2595 }; 2596 const lastBufferedEnd = function (a) { 2597 if (!a || !a.length || !a.end) { 2598 return; 2599 } 2600 2601 return a.end(a.length - 1); 2602 }; 2603 /** 2604 * A utility function to add up the amount of time in a timeRange 2605 * after a specified startTime. 2606 * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0 2607 * would return 40 as there are 40s seconds after 0 in the timeRange 2608 * 2609 * @param {TimeRange} range 2610 * The range to check against 2611 * @param {number} startTime 2612 * The time in the time range that you should start counting from 2613 * 2614 * @return {number} 2615 * The number of seconds in the buffer passed the specified time. 2616 */ 2617 2618 const timeAheadOf = function (range, startTime) { 2619 let time = 0; 2620 2621 if (!range || !range.length) { 2622 return time; 2623 } 2624 2625 for (let i = 0; i < range.length; i++) { 2626 const start = range.start(i); 2627 const end = range.end(i); // startTime is after this range entirely 2628 2629 if (startTime > end) { 2630 continue; 2631 } // startTime is within this range 2632 2633 2634 if (startTime > start && startTime <= end) { 2635 time += end - startTime; 2636 continue; 2637 } // startTime is before this range. 2638 2639 2640 time += end - start; 2641 } 2642 2643 return time; 2644 }; 2645 2646 /** 2647 * @file playlist.js 2648 * 2649 * Playlist related utilities. 2650 */ 2651 /** 2652 * Get the duration of a segment, with special cases for 2653 * llhls segments that do not have a duration yet. 2654 * 2655 * @param {Object} playlist 2656 * the playlist that the segment belongs to. 2657 * @param {Object} segment 2658 * the segment to get a duration for. 2659 * 2660 * @return {number} 2661 * the segment duration 2662 */ 2663 2664 const segmentDurationWithParts = (playlist, segment) => { 2665 // if this isn't a preload segment 2666 // then we will have a segment duration that is accurate. 2667 if (!segment.preload) { 2668 return segment.duration; 2669 } // otherwise we have to add up parts and preload hints 2670 // to get an up to date duration. 2671 2672 2673 let result = 0; 2674 (segment.parts || []).forEach(function (p) { 2675 result += p.duration; 2676 }); // for preload hints we have to use partTargetDuration 2677 // as they won't even have a duration yet. 2678 2679 (segment.preloadHints || []).forEach(function (p) { 2680 if (p.type === 'PART') { 2681 result += playlist.partTargetDuration; 2682 } 2683 }); 2684 return result; 2685 }; 2686 /** 2687 * A function to get a combined list of parts and segments with durations 2688 * and indexes. 2689 * 2690 * @param {Playlist} playlist the playlist to get the list for. 2691 * 2692 * @return {Array} The part/segment list. 2693 */ 2694 2695 const getPartsAndSegments = playlist => (playlist.segments || []).reduce((acc, segment, si) => { 2696 if (segment.parts) { 2697 segment.parts.forEach(function (part, pi) { 2698 acc.push({ 2699 duration: part.duration, 2700 segmentIndex: si, 2701 partIndex: pi, 2702 part, 2703 segment 2704 }); 2705 }); 2706 } else { 2707 acc.push({ 2708 duration: segment.duration, 2709 segmentIndex: si, 2710 partIndex: null, 2711 segment, 2712 part: null 2713 }); 2714 } 2715 2716 return acc; 2717 }, []); 2718 const getLastParts = media => { 2719 const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1]; 2720 return lastSegment && lastSegment.parts || []; 2721 }; 2722 const getKnownPartCount = ({ 2723 preloadSegment 2724 }) => { 2725 if (!preloadSegment) { 2726 return; 2727 } 2728 2729 const { 2730 parts, 2731 preloadHints 2732 } = preloadSegment; 2733 let partCount = (preloadHints || []).reduce((count, hint) => count + (hint.type === 'PART' ? 1 : 0), 0); 2734 partCount += parts && parts.length ? parts.length : 0; 2735 return partCount; 2736 }; 2737 /** 2738 * Get the number of seconds to delay from the end of a 2739 * live playlist. 2740 * 2741 * @param {Playlist} main the main playlist 2742 * @param {Playlist} media the media playlist 2743 * @return {number} the hold back in seconds. 2744 */ 2745 2746 const liveEdgeDelay = (main, media) => { 2747 if (media.endList) { 2748 return 0; 2749 } // dash suggestedPresentationDelay trumps everything 2750 2751 2752 if (main && main.suggestedPresentationDelay) { 2753 return main.suggestedPresentationDelay; 2754 } 2755 2756 const hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first 2757 2758 if (hasParts && media.serverControl && media.serverControl.partHoldBack) { 2759 return media.serverControl.partHoldBack; 2760 } else if (hasParts && media.partTargetDuration) { 2761 return media.partTargetDuration * 3; // finally look for full segment delays 2762 } else if (media.serverControl && media.serverControl.holdBack) { 2763 return media.serverControl.holdBack; 2764 } else if (media.targetDuration) { 2765 return media.targetDuration * 3; 2766 } 2767 2768 return 0; 2769 }; 2770 /** 2771 * walk backward until we find a duration we can use 2772 * or return a failure 2773 * 2774 * @param {Playlist} playlist the playlist to walk through 2775 * @param {Number} endSequence the mediaSequence to stop walking on 2776 */ 2777 2778 const backwardDuration = function (playlist, endSequence) { 2779 let result = 0; 2780 let i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following 2781 // the interval, use it 2782 2783 let segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline 2784 // information that is earlier than endSequence 2785 2786 if (segment) { 2787 if (typeof segment.start !== 'undefined') { 2788 return { 2789 result: segment.start, 2790 precise: true 2791 }; 2792 } 2793 2794 if (typeof segment.end !== 'undefined') { 2795 return { 2796 result: segment.end - segment.duration, 2797 precise: true 2798 }; 2799 } 2800 } 2801 2802 while (i--) { 2803 segment = playlist.segments[i]; 2804 2805 if (typeof segment.end !== 'undefined') { 2806 return { 2807 result: result + segment.end, 2808 precise: true 2809 }; 2810 } 2811 2812 result += segmentDurationWithParts(playlist, segment); 2813 2814 if (typeof segment.start !== 'undefined') { 2815 return { 2816 result: result + segment.start, 2817 precise: true 2818 }; 2819 } 2820 } 2821 2822 return { 2823 result, 2824 precise: false 2825 }; 2826 }; 2827 /** 2828 * walk forward until we find a duration we can use 2829 * or return a failure 2830 * 2831 * @param {Playlist} playlist the playlist to walk through 2832 * @param {number} endSequence the mediaSequence to stop walking on 2833 */ 2834 2835 2836 const forwardDuration = function (playlist, endSequence) { 2837 let result = 0; 2838 let segment; 2839 let i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline 2840 // information 2841 2842 for (; i < playlist.segments.length; i++) { 2843 segment = playlist.segments[i]; 2844 2845 if (typeof segment.start !== 'undefined') { 2846 return { 2847 result: segment.start - result, 2848 precise: true 2849 }; 2850 } 2851 2852 result += segmentDurationWithParts(playlist, segment); 2853 2854 if (typeof segment.end !== 'undefined') { 2855 return { 2856 result: segment.end - result, 2857 precise: true 2858 }; 2859 } 2860 } // indicate we didn't find a useful duration estimate 2861 2862 2863 return { 2864 result: -1, 2865 precise: false 2866 }; 2867 }; 2868 /** 2869 * Calculate the media duration from the segments associated with a 2870 * playlist. The duration of a subinterval of the available segments 2871 * may be calculated by specifying an end index. 2872 * 2873 * @param {Object} playlist a media playlist object 2874 * @param {number=} endSequence an exclusive upper boundary 2875 * for the playlist. Defaults to playlist length. 2876 * @param {number} expired the amount of time that has dropped 2877 * off the front of the playlist in a live scenario 2878 * @return {number} the duration between the first available segment 2879 * and end index. 2880 */ 2881 2882 2883 const intervalDuration = function (playlist, endSequence, expired) { 2884 if (typeof endSequence === 'undefined') { 2885 endSequence = playlist.mediaSequence + playlist.segments.length; 2886 } 2887 2888 if (endSequence < playlist.mediaSequence) { 2889 return 0; 2890 } // do a backward walk to estimate the duration 2891 2892 2893 const backward = backwardDuration(playlist, endSequence); 2894 2895 if (backward.precise) { 2896 // if we were able to base our duration estimate on timing 2897 // information provided directly from the Media Source, return 2898 // it 2899 return backward.result; 2900 } // walk forward to see if a precise duration estimate can be made 2901 // that way 2902 2903 2904 const forward = forwardDuration(playlist, endSequence); 2905 2906 if (forward.precise) { 2907 // we found a segment that has been buffered and so it's 2908 // position is known precisely 2909 return forward.result; 2910 } // return the less-precise, playlist-based duration estimate 2911 2912 2913 return backward.result + expired; 2914 }; 2915 /** 2916 * Calculates the duration of a playlist. If a start and end index 2917 * are specified, the duration will be for the subset of the media 2918 * timeline between those two indices. The total duration for live 2919 * playlists is always Infinity. 2920 * 2921 * @param {Object} playlist a media playlist object 2922 * @param {number=} endSequence an exclusive upper 2923 * boundary for the playlist. Defaults to the playlist media 2924 * sequence number plus its length. 2925 * @param {number=} expired the amount of time that has 2926 * dropped off the front of the playlist in a live scenario 2927 * @return {number} the duration between the start index and end 2928 * index. 2929 */ 2930 2931 2932 const duration = function (playlist, endSequence, expired) { 2933 if (!playlist) { 2934 return 0; 2935 } 2936 2937 if (typeof expired !== 'number') { 2938 expired = 0; 2939 } // if a slice of the total duration is not requested, use 2940 // playlist-level duration indicators when they're present 2941 2942 2943 if (typeof endSequence === 'undefined') { 2944 // if present, use the duration specified in the playlist 2945 if (playlist.totalDuration) { 2946 return playlist.totalDuration; 2947 } // duration should be Infinity for live playlists 2948 2949 2950 if (!playlist.endList) { 2951 return window.Infinity; 2952 } 2953 } // calculate the total duration based on the segment durations 2954 2955 2956 return intervalDuration(playlist, endSequence, expired); 2957 }; 2958 /** 2959 * Calculate the time between two indexes in the current playlist 2960 * neight the start- nor the end-index need to be within the current 2961 * playlist in which case, the targetDuration of the playlist is used 2962 * to approximate the durations of the segments 2963 * 2964 * @param {Array} options.durationList list to iterate over for durations. 2965 * @param {number} options.defaultDuration duration to use for elements before or after the durationList 2966 * @param {number} options.startIndex partsAndSegments index to start 2967 * @param {number} options.endIndex partsAndSegments index to end. 2968 * @return {number} the number of seconds between startIndex and endIndex 2969 */ 2970 2971 const sumDurations = function ({ 2972 defaultDuration, 2973 durationList, 2974 startIndex, 2975 endIndex 2976 }) { 2977 let durations = 0; 2978 2979 if (startIndex > endIndex) { 2980 [startIndex, endIndex] = [endIndex, startIndex]; 2981 } 2982 2983 if (startIndex < 0) { 2984 for (let i = startIndex; i < Math.min(0, endIndex); i++) { 2985 durations += defaultDuration; 2986 } 2987 2988 startIndex = 0; 2989 } 2990 2991 for (let i = startIndex; i < endIndex; i++) { 2992 durations += durationList[i].duration; 2993 } 2994 2995 return durations; 2996 }; 2997 /** 2998 * Calculates the playlist end time 2999 * 3000 * @param {Object} playlist a media playlist object 3001 * @param {number=} expired the amount of time that has 3002 * dropped off the front of the playlist in a live scenario 3003 * @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 3004 * playlist end calculation should consider the safe live end 3005 * (truncate the playlist end by three segments). This is normally 3006 * used for calculating the end of the playlist's seekable range. 3007 * This takes into account the value of liveEdgePadding. 3008 * Setting liveEdgePadding to 0 is equivalent to setting this to false. 3009 * @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds. 3010 * If this is provided, it is used in the safe live end calculation. 3011 * Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent. 3012 * Corresponds to suggestedPresentationDelay in DASH manifests. 3013 * @return {number} the end time of playlist 3014 * @function playlistEnd 3015 */ 3016 3017 const playlistEnd = function (playlist, expired, useSafeLiveEnd, liveEdgePadding) { 3018 if (!playlist || !playlist.segments) { 3019 return null; 3020 } 3021 3022 if (playlist.endList) { 3023 return duration(playlist); 3024 } 3025 3026 if (expired === null) { 3027 return null; 3028 } 3029 3030 expired = expired || 0; 3031 let lastSegmentEndTime = intervalDuration(playlist, playlist.mediaSequence + playlist.segments.length, expired); 3032 3033 if (useSafeLiveEnd) { 3034 liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist); 3035 lastSegmentEndTime -= liveEdgePadding; 3036 } // don't return a time less than zero 3037 3038 3039 return Math.max(0, lastSegmentEndTime); 3040 }; 3041 /** 3042 * Calculates the interval of time that is currently seekable in a 3043 * playlist. The returned time ranges are relative to the earliest 3044 * moment in the specified playlist that is still available. A full 3045 * seekable implementation for live streams would need to offset 3046 * these values by the duration of content that has expired from the 3047 * stream. 3048 * 3049 * @param {Object} playlist a media playlist object 3050 * dropped off the front of the playlist in a live scenario 3051 * @param {number=} expired the amount of time that has 3052 * dropped off the front of the playlist in a live scenario 3053 * @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds. 3054 * Corresponds to suggestedPresentationDelay in DASH manifests. 3055 * @return {TimeRanges} the periods of time that are valid targets 3056 * for seeking 3057 */ 3058 3059 const seekable = function (playlist, expired, liveEdgePadding) { 3060 const useSafeLiveEnd = true; 3061 const seekableStart = expired || 0; 3062 let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding); 3063 3064 if (seekableEnd === null) { 3065 return createTimeRanges(); 3066 } // Clamp seekable end since it can not be less than the seekable start 3067 3068 3069 if (seekableEnd < seekableStart) { 3070 seekableEnd = seekableStart; 3071 } 3072 3073 return createTimeRanges(seekableStart, seekableEnd); 3074 }; 3075 /** 3076 * Determine the index and estimated starting time of the segment that 3077 * contains a specified playback position in a media playlist. 3078 * 3079 * @param {Object} options.playlist the media playlist to query 3080 * @param {number} options.currentTime The number of seconds since the earliest 3081 * possible position to determine the containing segment for 3082 * @param {number} options.startTime the time when the segment/part starts 3083 * @param {number} options.startingSegmentIndex the segment index to start looking at. 3084 * @param {number?} [options.startingPartIndex] the part index to look at within the segment. 3085 * 3086 * @return {Object} an object with partIndex, segmentIndex, and startTime. 3087 */ 3088 3089 const getMediaInfoForTime = function ({ 3090 playlist, 3091 currentTime, 3092 startingSegmentIndex, 3093 startingPartIndex, 3094 startTime, 3095 exactManifestTimings 3096 }) { 3097 let time = currentTime - startTime; 3098 const partsAndSegments = getPartsAndSegments(playlist); 3099 let startIndex = 0; 3100 3101 for (let i = 0; i < partsAndSegments.length; i++) { 3102 const partAndSegment = partsAndSegments[i]; 3103 3104 if (startingSegmentIndex !== partAndSegment.segmentIndex) { 3105 continue; 3106 } // skip this if part index does not match. 3107 3108 3109 if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) { 3110 continue; 3111 } 3112 3113 startIndex = i; 3114 break; 3115 } 3116 3117 if (time < 0) { 3118 // Walk backward from startIndex in the playlist, adding durations 3119 // until we find a segment that contains `time` and return it 3120 if (startIndex > 0) { 3121 for (let i = startIndex - 1; i >= 0; i--) { 3122 const partAndSegment = partsAndSegments[i]; 3123 time += partAndSegment.duration; 3124 3125 if (exactManifestTimings) { 3126 if (time < 0) { 3127 continue; 3128 } 3129 } else if (time + TIME_FUDGE_FACTOR <= 0) { 3130 continue; 3131 } 3132 3133 return { 3134 partIndex: partAndSegment.partIndex, 3135 segmentIndex: partAndSegment.segmentIndex, 3136 startTime: startTime - sumDurations({ 3137 defaultDuration: playlist.targetDuration, 3138 durationList: partsAndSegments, 3139 startIndex, 3140 endIndex: i 3141 }) 3142 }; 3143 } 3144 } // We were unable to find a good segment within the playlist 3145 // so select the first segment 3146 3147 3148 return { 3149 partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, 3150 segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, 3151 startTime: currentTime 3152 }; 3153 } // When startIndex is negative, we first walk forward to first segment 3154 // adding target durations. If we "run out of time" before getting to 3155 // the first segment, return the first segment 3156 3157 3158 if (startIndex < 0) { 3159 for (let i = startIndex; i < 0; i++) { 3160 time -= playlist.targetDuration; 3161 3162 if (time < 0) { 3163 return { 3164 partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, 3165 segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, 3166 startTime: currentTime 3167 }; 3168 } 3169 } 3170 3171 startIndex = 0; 3172 } // Walk forward from startIndex in the playlist, subtracting durations 3173 // until we find a segment that contains `time` and return it 3174 3175 3176 for (let i = startIndex; i < partsAndSegments.length; i++) { 3177 const partAndSegment = partsAndSegments[i]; 3178 time -= partAndSegment.duration; 3179 const canUseFudgeFactor = partAndSegment.duration > TIME_FUDGE_FACTOR; 3180 const isExactlyAtTheEnd = time === 0; 3181 const isExtremelyCloseToTheEnd = canUseFudgeFactor && time + TIME_FUDGE_FACTOR >= 0; 3182 3183 if (isExactlyAtTheEnd || isExtremelyCloseToTheEnd) { 3184 // 1) We are exactly at the end of the current segment. 3185 // 2) We are extremely close to the end of the current segment (The difference is less than 1 / 30). 3186 // We may encounter this situation when 3187 // we don't have exact match between segment duration info in the manifest and the actual duration of the segment 3188 // For example: 3189 // We appended 3 segments 10 seconds each, meaning we should have 30 sec buffered, 3190 // but we the actual buffered is 29.99999 3191 // 3192 // In both cases: 3193 // if we passed current time -> it means that we already played current segment 3194 // if we passed buffered.end -> it means that this segment is already loaded and buffered 3195 // we should select the next segment if we have one: 3196 if (i !== partsAndSegments.length - 1) { 3197 continue; 3198 } 3199 } 3200 3201 if (exactManifestTimings) { 3202 if (time > 0) { 3203 continue; 3204 } 3205 } else if (time - TIME_FUDGE_FACTOR >= 0) { 3206 continue; 3207 } 3208 3209 return { 3210 partIndex: partAndSegment.partIndex, 3211 segmentIndex: partAndSegment.segmentIndex, 3212 startTime: startTime + sumDurations({ 3213 defaultDuration: playlist.targetDuration, 3214 durationList: partsAndSegments, 3215 startIndex, 3216 endIndex: i 3217 }) 3218 }; 3219 } // We are out of possible candidates so load the last one... 3220 3221 3222 return { 3223 segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, 3224 partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, 3225 startTime: currentTime 3226 }; 3227 }; 3228 /** 3229 * Check whether the playlist is excluded or not. 3230 * 3231 * @param {Object} playlist the media playlist object 3232 * @return {boolean} whether the playlist is excluded or not 3233 * @function isExcluded 3234 */ 3235 3236 const isExcluded = function (playlist) { 3237 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 3238 }; 3239 /** 3240 * Check whether the playlist is compatible with current playback configuration or has 3241 * been excluded permanently for being incompatible. 3242 * 3243 * @param {Object} playlist the media playlist object 3244 * @return {boolean} whether the playlist is incompatible or not 3245 * @function isIncompatible 3246 */ 3247 3248 const isIncompatible = function (playlist) { 3249 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 3250 }; 3251 /** 3252 * Check whether the playlist is enabled or not. 3253 * 3254 * @param {Object} playlist the media playlist object 3255 * @return {boolean} whether the playlist is enabled or not 3256 * @function isEnabled 3257 */ 3258 3259 const isEnabled = function (playlist) { 3260 const excluded = isExcluded(playlist); 3261 return !playlist.disabled && !excluded; 3262 }; 3263 /** 3264 * Check whether the playlist has been manually disabled through the representations api. 3265 * 3266 * @param {Object} playlist the media playlist object 3267 * @return {boolean} whether the playlist is disabled manually or not 3268 * @function isDisabled 3269 */ 3270 3271 const isDisabled = function (playlist) { 3272 return playlist.disabled; 3273 }; 3274 /** 3275 * Returns whether the current playlist is an AES encrypted HLS stream 3276 * 3277 * @return {boolean} true if it's an AES encrypted HLS stream 3278 */ 3279 3280 const isAes = function (media) { 3281 for (let i = 0; i < media.segments.length; i++) { 3282 if (media.segments[i].key) { 3283 return true; 3284 } 3285 } 3286 3287 return false; 3288 }; 3289 /** 3290 * Checks if the playlist has a value for the specified attribute 3291 * 3292 * @param {string} attr 3293 * Attribute to check for 3294 * @param {Object} playlist 3295 * The media playlist object 3296 * @return {boolean} 3297 * Whether the playlist contains a value for the attribute or not 3298 * @function hasAttribute 3299 */ 3300 3301 const hasAttribute = function (attr, playlist) { 3302 return playlist.attributes && playlist.attributes[attr]; 3303 }; 3304 /** 3305 * Estimates the time required to complete a segment download from the specified playlist 3306 * 3307 * @param {number} segmentDuration 3308 * Duration of requested segment 3309 * @param {number} bandwidth 3310 * Current measured bandwidth of the player 3311 * @param {Object} playlist 3312 * The media playlist object 3313 * @param {number=} bytesReceived 3314 * Number of bytes already received for the request. Defaults to 0 3315 * @return {number|NaN} 3316 * The estimated time to request the segment. NaN if bandwidth information for 3317 * the given playlist is unavailable 3318 * @function estimateSegmentRequestTime 3319 */ 3320 3321 const estimateSegmentRequestTime = function (segmentDuration, bandwidth, playlist, bytesReceived = 0) { 3322 if (!hasAttribute('BANDWIDTH', playlist)) { 3323 return NaN; 3324 } 3325 3326 const size = segmentDuration * playlist.attributes.BANDWIDTH; 3327 return (size - bytesReceived * 8) / bandwidth; 3328 }; 3329 /* 3330 * Returns whether the current playlist is the lowest rendition 3331 * 3332 * @return {Boolean} true if on lowest rendition 3333 */ 3334 3335 const isLowestEnabledRendition = (main, media) => { 3336 if (main.playlists.length === 1) { 3337 return true; 3338 } 3339 3340 const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 3341 return main.playlists.filter(playlist => { 3342 if (!isEnabled(playlist)) { 3343 return false; 3344 } 3345 3346 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 3347 }).length === 0; 3348 }; 3349 const playlistMatch = (a, b) => { 3350 // both playlits are null 3351 // or only one playlist is non-null 3352 // no match 3353 if (!a && !b || !a && b || a && !b) { 3354 return false; 3355 } // playlist objects are the same, match 3356 3357 3358 if (a === b) { 3359 return true; 3360 } // first try to use id as it should be the most 3361 // accurate 3362 3363 3364 if (a.id && b.id && a.id === b.id) { 3365 return true; 3366 } // next try to use reslovedUri as it should be the 3367 // second most accurate. 3368 3369 3370 if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) { 3371 return true; 3372 } // finally try to use uri as it should be accurate 3373 // but might miss a few cases for relative uris 3374 3375 3376 if (a.uri && b.uri && a.uri === b.uri) { 3377 return true; 3378 } 3379 3380 return false; 3381 }; 3382 3383 const someAudioVariant = function (main, callback) { 3384 const AUDIO = main && main.mediaGroups && main.mediaGroups.AUDIO || {}; 3385 let found = false; 3386 3387 for (const groupName in AUDIO) { 3388 for (const label in AUDIO[groupName]) { 3389 found = callback(AUDIO[groupName][label]); 3390 3391 if (found) { 3392 break; 3393 } 3394 } 3395 3396 if (found) { 3397 break; 3398 } 3399 } 3400 3401 return !!found; 3402 }; 3403 3404 const isAudioOnly = main => { 3405 // we are audio only if we have no main playlists but do 3406 // have media group playlists. 3407 if (!main || !main.playlists || !main.playlists.length) { 3408 // without audio variants or playlists this 3409 // is not an audio only main. 3410 const found = someAudioVariant(main, variant => variant.playlists && variant.playlists.length || variant.uri); 3411 return found; 3412 } // if every playlist has only an audio codec it is audio only 3413 3414 3415 for (let i = 0; i < main.playlists.length; i++) { 3416 const playlist = main.playlists[i]; 3417 const CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist. 3418 3419 if (CODECS && CODECS.split(',').every(c => isAudioCodec(c))) { 3420 continue; 3421 } // playlist is in an audio group it is audio only 3422 3423 3424 const found = someAudioVariant(main, variant => playlistMatch(playlist, variant)); 3425 3426 if (found) { 3427 continue; 3428 } // if we make it here this playlist isn't audio and we 3429 // are not audio only 3430 3431 3432 return false; 3433 } // if we make it past every playlist without returning, then 3434 // this is an audio only playlist. 3435 3436 3437 return true; 3438 }; // exports 3439 3440 var Playlist = { 3441 liveEdgeDelay, 3442 duration, 3443 seekable, 3444 getMediaInfoForTime, 3445 isEnabled, 3446 isDisabled, 3447 isExcluded, 3448 isIncompatible, 3449 playlistEnd, 3450 isAes, 3451 hasAttribute, 3452 estimateSegmentRequestTime, 3453 isLowestEnabledRendition, 3454 isAudioOnly, 3455 playlistMatch, 3456 segmentDurationWithParts 3457 }; 3458 3459 const { 3460 log 3461 } = videojs__default["default"]; 3462 const createPlaylistID = (index, uri) => { 3463 return `${index}-${uri}`; 3464 }; // default function for creating a group id 3465 3466 const groupID = (type, group, label) => { 3467 return `placeholder-uri-${type}-${group}-${label}`; 3468 }; 3469 /** 3470 * Parses a given m3u8 playlist 3471 * 3472 * @param {Function} [onwarn] 3473 * a function to call when the parser triggers a warning event. 3474 * @param {Function} [oninfo] 3475 * a function to call when the parser triggers an info event. 3476 * @param {string} manifestString 3477 * The downloaded manifest string 3478 * @param {Object[]} [customTagParsers] 3479 * An array of custom tag parsers for the m3u8-parser instance 3480 * @param {Object[]} [customTagMappers] 3481 * An array of custom tag mappers for the m3u8-parser instance 3482 * @param {boolean} [llhls] 3483 * Whether to keep ll-hls features in the manifest after parsing. 3484 * @return {Object} 3485 * The manifest object 3486 */ 3487 3488 const parseManifest = ({ 3489 onwarn, 3490 oninfo, 3491 manifestString, 3492 customTagParsers = [], 3493 customTagMappers = [], 3494 llhls 3495 }) => { 3496 const parser = new Parser(); 3497 3498 if (onwarn) { 3499 parser.on('warn', onwarn); 3500 } 3501 3502 if (oninfo) { 3503 parser.on('info', oninfo); 3504 } 3505 3506 customTagParsers.forEach(customParser => parser.addParser(customParser)); 3507 customTagMappers.forEach(mapper => parser.addTagMapper(mapper)); 3508 parser.push(manifestString); 3509 parser.end(); 3510 const manifest = parser.manifest; // remove llhls features from the parsed manifest 3511 // if we don't want llhls support. 3512 3513 if (!llhls) { 3514 ['preloadSegment', 'skip', 'serverControl', 'renditionReports', 'partInf', 'partTargetDuration'].forEach(function (k) { 3515 if (manifest.hasOwnProperty(k)) { 3516 delete manifest[k]; 3517 } 3518 }); 3519 3520 if (manifest.segments) { 3521 manifest.segments.forEach(function (segment) { 3522 ['parts', 'preloadHints'].forEach(function (k) { 3523 if (segment.hasOwnProperty(k)) { 3524 delete segment[k]; 3525 } 3526 }); 3527 }); 3528 } 3529 } 3530 3531 if (!manifest.targetDuration) { 3532 let targetDuration = 10; 3533 3534 if (manifest.segments && manifest.segments.length) { 3535 targetDuration = manifest.segments.reduce((acc, s) => Math.max(acc, s.duration), 0); 3536 } 3537 3538 if (onwarn) { 3539 onwarn({ 3540 message: `manifest has no targetDuration defaulting to ${targetDuration}` 3541 }); 3542 } 3543 3544 manifest.targetDuration = targetDuration; 3545 } 3546 3547 const parts = getLastParts(manifest); 3548 3549 if (parts.length && !manifest.partTargetDuration) { 3550 const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0); 3551 3552 if (onwarn) { 3553 onwarn({ 3554 message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}` 3555 }); 3556 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.'); 3557 } 3558 3559 manifest.partTargetDuration = partTargetDuration; 3560 } 3561 3562 return manifest; 3563 }; 3564 /** 3565 * Loops through all supported media groups in main and calls the provided 3566 * callback for each group 3567 * 3568 * @param {Object} main 3569 * The parsed main manifest object 3570 * @param {Function} callback 3571 * Callback to call for each media group 3572 */ 3573 3574 const forEachMediaGroup$1 = (main, callback) => { 3575 if (!main.mediaGroups) { 3576 return; 3577 } 3578 3579 ['AUDIO', 'SUBTITLES'].forEach(mediaType => { 3580 if (!main.mediaGroups[mediaType]) { 3581 return; 3582 } 3583 3584 for (const groupKey in main.mediaGroups[mediaType]) { 3585 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 3586 const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey]; 3587 callback(mediaProperties, mediaType, groupKey, labelKey); 3588 } 3589 } 3590 }); 3591 }; 3592 /** 3593 * Adds properties and attributes to the playlist to keep consistent functionality for 3594 * playlists throughout VHS. 3595 * 3596 * @param {Object} config 3597 * Arguments object 3598 * @param {Object} config.playlist 3599 * The media playlist 3600 * @param {string} [config.uri] 3601 * The uri to the media playlist (if media playlist is not from within a main 3602 * playlist) 3603 * @param {string} id 3604 * ID to use for the playlist 3605 */ 3606 3607 const setupMediaPlaylist = ({ 3608 playlist, 3609 uri, 3610 id 3611 }) => { 3612 playlist.id = id; 3613 playlist.playlistErrors_ = 0; 3614 3615 if (uri) { 3616 // For media playlists, m3u8-parser does not have access to a URI, as HLS media 3617 // playlists do not contain their own source URI, but one is needed for consistency in 3618 // VHS. 3619 playlist.uri = uri; 3620 } // For HLS main playlists, even though certain attributes MUST be defined, the 3621 // stream may still be played without them. 3622 // For HLS media playlists, m3u8-parser does not attach an attributes object to the 3623 // manifest. 3624 // 3625 // To avoid undefined reference errors through the project, and make the code easier 3626 // to write/read, add an empty attributes object for these cases. 3627 3628 3629 playlist.attributes = playlist.attributes || {}; 3630 }; 3631 /** 3632 * Adds ID, resolvedUri, and attributes properties to each playlist of the main, where 3633 * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to 3634 * playlist references to the playlists array. 3635 * 3636 * @param {Object} main 3637 * The main playlist 3638 */ 3639 3640 const setupMediaPlaylists = main => { 3641 let i = main.playlists.length; 3642 3643 while (i--) { 3644 const playlist = main.playlists[i]; 3645 setupMediaPlaylist({ 3646 playlist, 3647 id: createPlaylistID(i, playlist.uri) 3648 }); 3649 playlist.resolvedUri = resolveUrl(main.uri, playlist.uri); 3650 main.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility 3651 3652 main.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute, 3653 // the stream can be played without it. Although an attributes property may have been 3654 // added to the playlist to prevent undefined references, issue a warning to fix the 3655 // manifest. 3656 3657 if (!playlist.attributes.BANDWIDTH) { 3658 log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 3659 } 3660 } 3661 }; 3662 /** 3663 * Adds resolvedUri properties to each media group. 3664 * 3665 * @param {Object} main 3666 * The main playlist 3667 */ 3668 3669 const resolveMediaGroupUris = main => { 3670 forEachMediaGroup$1(main, properties => { 3671 if (properties.uri) { 3672 properties.resolvedUri = resolveUrl(main.uri, properties.uri); 3673 } 3674 }); 3675 }; 3676 /** 3677 * Creates a main playlist wrapper to insert a sole media playlist into. 3678 * 3679 * @param {Object} media 3680 * Media playlist 3681 * @param {string} uri 3682 * The media URI 3683 * 3684 * @return {Object} 3685 * main playlist 3686 */ 3687 3688 const mainForMedia = (media, uri) => { 3689 const id = createPlaylistID(0, uri); 3690 const main = { 3691 mediaGroups: { 3692 'AUDIO': {}, 3693 'VIDEO': {}, 3694 'CLOSED-CAPTIONS': {}, 3695 'SUBTITLES': {} 3696 }, 3697 uri: window.location.href, 3698 resolvedUri: window.location.href, 3699 playlists: [{ 3700 uri, 3701 id, 3702 resolvedUri: uri, 3703 // m3u8-parser does not attach an attributes property to media playlists so make 3704 // sure that the property is attached to avoid undefined reference errors 3705 attributes: {} 3706 }] 3707 }; // set up ID reference 3708 3709 main.playlists[id] = main.playlists[0]; // URI reference added for backwards compatibility 3710 3711 main.playlists[uri] = main.playlists[0]; 3712 return main; 3713 }; 3714 /** 3715 * Does an in-place update of the main manifest to add updated playlist URI references 3716 * as well as other properties needed by VHS that aren't included by the parser. 3717 * 3718 * @param {Object} main 3719 * main manifest object 3720 * @param {string} uri 3721 * The source URI 3722 * @param {function} createGroupID 3723 * A function to determine how to create the groupID for mediaGroups 3724 */ 3725 3726 const addPropertiesToMain = (main, uri, createGroupID = groupID) => { 3727 main.uri = uri; 3728 3729 for (let i = 0; i < main.playlists.length; i++) { 3730 if (!main.playlists[i].uri) { 3731 // Set up phony URIs for the playlists since playlists are referenced by their URIs 3732 // throughout VHS, but some formats (e.g., DASH) don't have external URIs 3733 // TODO: consider adding dummy URIs in mpd-parser 3734 const phonyUri = `placeholder-uri-${i}`; 3735 main.playlists[i].uri = phonyUri; 3736 } 3737 } 3738 3739 const audioOnlyMain = isAudioOnly(main); 3740 forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => { 3741 // add a playlist array under properties 3742 if (!properties.playlists || !properties.playlists.length) { 3743 // If the manifest is audio only and this media group does not have a uri, check 3744 // if the media group is located in the main list of playlists. If it is, don't add 3745 // placeholder properties as it shouldn't be considered an alternate audio track. 3746 if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) { 3747 for (let i = 0; i < main.playlists.length; i++) { 3748 const p = main.playlists[i]; 3749 3750 if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) { 3751 return; 3752 } 3753 } 3754 } 3755 3756 properties.playlists = [_extends({}, properties)]; 3757 } 3758 3759 properties.playlists.forEach(function (p, i) { 3760 const groupId = createGroupID(mediaType, groupKey, labelKey, p); 3761 const id = createPlaylistID(i, groupId); 3762 3763 if (p.uri) { 3764 p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri); 3765 } else { 3766 // DEPRECATED, this has been added to prevent a breaking change. 3767 // previously we only ever had a single media group playlist, so 3768 // we mark the first playlist uri without prepending the index as we used to 3769 // ideally we would do all of the playlists the same way. 3770 p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use 3771 // the placeholder again 3772 3773 p.resolvedUri = p.uri; 3774 } 3775 3776 p.id = p.id || id; // add an empty attributes object, all playlists are 3777 // expected to have this. 3778 3779 p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility) 3780 3781 main.playlists[p.id] = p; 3782 main.playlists[p.uri] = p; 3783 }); 3784 }); 3785 setupMediaPlaylists(main); 3786 resolveMediaGroupUris(main); 3787 }; 3788 3789 class DateRangesStorage { 3790 constructor() { 3791 this.offset_ = null; 3792 this.pendingDateRanges_ = new Map(); 3793 this.processedDateRanges_ = new Map(); 3794 } 3795 3796 setOffset(segments = []) { 3797 // already set 3798 if (this.offset_ !== null) { 3799 return; 3800 } // no segment to process 3801 3802 3803 if (!segments.length) { 3804 return; 3805 } 3806 3807 const [firstSegment] = segments; // no program date time 3808 3809 if (firstSegment.programDateTime === undefined) { 3810 return; 3811 } // Set offset as ProgramDateTime for the very first segment of the very first playlist load: 3812 3813 3814 this.offset_ = firstSegment.programDateTime / 1000; 3815 } 3816 3817 setPendingDateRanges(dateRanges = []) { 3818 if (!dateRanges.length) { 3819 return; 3820 } 3821 3822 const [dateRange] = dateRanges; 3823 const startTime = dateRange.startDate.getTime(); 3824 this.trimProcessedDateRanges_(startTime); 3825 this.pendingDateRanges_ = dateRanges.reduce((map, pendingDateRange) => { 3826 map.set(pendingDateRange.id, pendingDateRange); 3827 return map; 3828 }, new Map()); 3829 } 3830 3831 processDateRange(dateRange) { 3832 this.pendingDateRanges_.delete(dateRange.id); 3833 this.processedDateRanges_.set(dateRange.id, dateRange); 3834 } 3835 3836 getDateRangesToProcess() { 3837 if (this.offset_ === null) { 3838 return []; 3839 } 3840 3841 const dateRangeClasses = {}; 3842 const dateRangesToProcess = []; 3843 this.pendingDateRanges_.forEach((dateRange, id) => { 3844 if (this.processedDateRanges_.has(id)) { 3845 return; 3846 } 3847 3848 dateRange.startTime = dateRange.startDate.getTime() / 1000 - this.offset_; 3849 3850 dateRange.processDateRange = () => this.processDateRange(dateRange); 3851 3852 dateRangesToProcess.push(dateRange); 3853 3854 if (!dateRange.class) { 3855 return; 3856 } 3857 3858 if (dateRangeClasses[dateRange.class]) { 3859 const length = dateRangeClasses[dateRange.class].push(dateRange); 3860 dateRange.classListIndex = length - 1; 3861 } else { 3862 dateRangeClasses[dateRange.class] = [dateRange]; 3863 dateRange.classListIndex = 0; 3864 } 3865 }); 3866 3867 for (const dateRange of dateRangesToProcess) { 3868 const classList = dateRangeClasses[dateRange.class] || []; 3869 3870 if (dateRange.endDate) { 3871 dateRange.endTime = dateRange.endDate.getTime() / 1000 - this.offset_; 3872 } else if (dateRange.endOnNext && classList[dateRange.classListIndex + 1]) { 3873 dateRange.endTime = classList[dateRange.classListIndex + 1].startTime; 3874 } else if (dateRange.duration) { 3875 dateRange.endTime = dateRange.startTime + dateRange.duration; 3876 } else if (dateRange.plannedDuration) { 3877 dateRange.endTime = dateRange.startTime + dateRange.plannedDuration; 3878 } else { 3879 dateRange.endTime = dateRange.startTime; 3880 } 3881 } 3882 3883 return dateRangesToProcess; 3884 } 3885 3886 trimProcessedDateRanges_(startTime) { 3887 const copy = new Map(this.processedDateRanges_); 3888 copy.forEach((dateRange, id) => { 3889 if (dateRange.startDate.getTime() < startTime) { 3890 this.processedDateRanges_.delete(id); 3891 } 3892 }); 3893 } 3894 3895 } 3896 3897 const
3897{ 3898 EventTarget: EventTarget$1 3899 } = videojs__default["default"]; 3900 3901 const addLLHLSQueryDirectives = (uri, media) => { 3902 if (media.endList || !media.serverControl) { 3903 return uri; 3904 } 3905 3906 const parameters = {}; 3907 3908 if (media.serverControl.canBlockReload) { 3909 const { 3910 preloadSegment 3911 } = media; // next msn is a zero based value, length is not. 3912 3913 let nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely 3914 // that we are going to request a part of that preload segment. 3915 // the logic below is used to determine that. 3916 3917 if (preloadSegment) { 3918 const parts = preloadSegment.parts || []; // _HLS_part is a zero based index 3919 3920 const nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the
vendor: 26,248 bytes, lines 3921-4726
3921 // length of parts, then we know we had part preload hints 3922 // and we need to add the _HLS_part= query 3923 3924 if (nextPart > -1 && nextPart !== parts.length - 1) { 3925 // add existing parts to our preload hints 3926 // eslint-disable-next-line 3927 parameters._HLS_part = nextPart; 3928 } // this if statement makes sure that we request the msn 3929 // of the preload segment if: 3930 // 1. the preload segment had parts (and was not yet a full segment) 3931 // but was added to our segments array 3932 // 2. the preload segment had preload hints for parts that are not in 3933 // the manifest yet. 3934 // in all other cases we want the segment after the preload segment 3935 // which will be given by using media.segments.length because it is 1 based 3936 // rather than 0 based. 3937 3938 3939 if (nextPart > -1 || parts.length) { 3940 nextMSN--; 3941 } 3942 } // add _HLS_msn= in front of any _HLS_part query 3943 // eslint-disable-next-line 3944 3945 3946 parameters._HLS_msn = nextMSN; 3947 } 3948 3949 if (media.serverControl && media.serverControl.canSkipUntil) { 3950 // add _HLS_skip= infront of all other queries. 3951 // eslint-disable-next-line 3952 parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES'; 3953 } 3954 3955 if (Object.keys(parameters).length) { 3956 const parsedUri = new window.URL(uri); 3957 ['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) { 3958 if (!parameters.hasOwnProperty(name)) { 3959 return; 3960 } 3961 3962 parsedUri.searchParams.set(name, parameters[name]); 3963 }); 3964 uri = parsedUri.toString(); 3965 } 3966 3967 return uri; 3968 }; 3969 /** 3970 * Returns a new segment object with properties and 3971 * the parts array merged. 3972 * 3973 * @param {Object} a the old segment 3974 * @param {Object} b the new segment 3975 * 3976 * @return {Object} the merged segment 3977 */ 3978 3979 3980 const updateSegment = (a, b) => { 3981 if (!a) { 3982 return b; 3983 } 3984 3985 const result = merge$1(a, b); // if only the old segment has preload hints 3986 // and the new one does not, remove preload hints. 3987 3988 if (a.preloadHints && !b.preloadHints) { 3989 delete result.preloadHints; 3990 } // if only the old segment has parts 3991 // then the parts are no longer valid 3992 3993 3994 if (a.parts && !b.parts) { 3995 delete result.parts; // if both segments have parts 3996 // copy part propeties from the old segment 3997 // to the new one. 3998 } else if (a.parts && b.parts) { 3999 for (let i = 0; i < b.parts.length; i++) { 4000 if (a.parts && a.parts[i]) { 4001 result.parts[i] = merge$1(a.parts[i], b.parts[i]); 4002 } 4003 } 4004 } // set skipped to false for segments that have 4005 // have had information merged from the old segment. 4006 4007 4008 if (!a.skipped && b.skipped) { 4009 result.skipped = false; 4010 } // set preload to false for segments that have 4011 // had information added in the new segment. 4012 4013 4014 if (a.preload && !b.preload) { 4015 result.preload = false; 4016 } 4017 4018 return result; 4019 }; 4020 /** 4021 * Returns a new array of segments that is the result of merging 4022 * properties from an older list of segments onto an updated 4023 * list. No properties on the updated playlist will be ovewritten. 4024 * 4025 * @param {Array} original the outdated list of segments 4026 * @param {Array} update the updated list of segments 4027 * @param {number=} offset the index of the first update 4028 * segment in the original segment list. For non-live playlists, 4029 * this should always be zero and does not need to be 4030 * specified. For live playlists, it should be the difference 4031 * between the media sequence numbers in the original and updated 4032 * playlists. 4033 * @return {Array} a list of merged segment objects 4034 */ 4035 4036 const updateSegments = (original, update, offset) => { 4037 const oldSegments = original.slice(); 4038 const newSegments = update.slice(); 4039 offset = offset || 0; 4040 const result = []; 4041 let currentMap; 4042 4043 for (let newIndex = 0; newIndex < newSegments.length; newIndex++) { 4044 const oldSegment = oldSegments[newIndex + offset]; 4045 const newSegment = newSegments[newIndex]; 4046 4047 if (oldSegment) { 4048 currentMap = oldSegment.map || currentMap; 4049 result.push(updateSegment(oldSegment, newSegment)); 4050 } else { 4051 // carry over map to new segment if it is missing 4052 if (currentMap && !newSegment.map) { 4053 newSegment.map = currentMap; 4054 } 4055 4056 result.push(newSegment); 4057 } 4058 } 4059 4060 return result; 4061 }; 4062 const resolveSegmentUris = (segment, baseUri) => { 4063 // preloadSegment will not have a uri at all 4064 // as the segment isn't actually in the manifest yet, only parts 4065 if (!segment.resolvedUri && segment.uri) { 4066 segment.resolvedUri = resolveUrl(baseUri, segment.uri); 4067 } 4068 4069 if (segment.key && !segment.key.resolvedUri) { 4070 segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri); 4071 } 4072 4073 if (segment.map && !segment.map.resolvedUri) { 4074 segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri); 4075 } 4076 4077 if (segment.map && segment.map.key && !segment.map.key.resolvedUri) { 4078 segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri); 4079 } 4080 4081 if (segment.parts && segment.parts.length) { 4082 segment.parts.forEach(p => { 4083 if (p.resolvedUri) { 4084 return; 4085 } 4086 4087 p.resolvedUri = resolveUrl(baseUri, p.uri); 4088 }); 4089 } 4090 4091 if (segment.preloadHints && segment.preloadHints.length) { 4092 segment.preloadHints.forEach(p => { 4093 if (p.resolvedUri) { 4094 return; 4095 } 4096 4097 p.resolvedUri = resolveUrl(baseUri, p.uri); 4098 }); 4099 } 4100 }; 4101 4102 const getAllSegments = function (media) { 4103 const segments = media.segments || []; 4104 const preloadSegment = media.preloadSegment; // a preloadSegment with only preloadHints is not currently 4105 // a usable segment, only include a preloadSegment that has 4106 // parts. 4107 4108 if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) { 4109 // if preloadHints has a MAP that means that the 4110 // init segment is going to change. We cannot use any of the parts 4111 // from this preload segment. 4112 if (preloadSegment.preloadHints) { 4113 for (let i = 0; i < preloadSegment.preloadHints.length; i++) { 4114 if (preloadSegment.preloadHints[i].type === 'MAP') { 4115 return segments; 4116 } 4117 } 4118 } // set the duration for our preload segment to target duration. 4119 4120 4121 preloadSegment.duration = media.targetDuration; 4122 preloadSegment.preload = true; 4123 segments.push(preloadSegment); 4124 } 4125 4126 return segments; 4127 }; // consider the playlist unchanged if the playlist object is the same or 4128 // the number of segments is equal, the media sequence number is unchanged, 4129 // and this playlist hasn't become the end of the playlist 4130 4131 4132 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; 4133 /** 4134 * Returns a new main playlist that is the result of merging an 4135 * updated media playlist into the original version. If the 4136 * updated media playlist does not match any of the playlist 4137 * entries in the original main playlist, null is returned. 4138 * 4139 * @param {Object} main a parsed main M3U8 object 4140 * @param {Object} media a parsed media M3U8 object 4141 * @return {Object} a new object that represents the original 4142 * main playlist with the updated media playlist merged in, or 4143 * null if the merge produced no change. 4144 */ 4145 4146 const updateMain$1 = (main, newMedia, unchangedCheck = isPlaylistUnchanged) => { 4147 const result = merge$1(main, {}); 4148 const oldMedia = result.playlists[newMedia.id]; 4149 4150 if (!oldMedia) { 4151 return null; 4152 } 4153 4154 if (unchangedCheck(oldMedia, newMedia)) { 4155 return null; 4156 } 4157 4158 newMedia.segments = getAllSegments(newMedia); 4159 const mergedPlaylist = merge$1(oldMedia, newMedia); // always use the new media's preload segment 4160 4161 if (mergedPlaylist.preloadSegment && !newMedia.preloadSegment) { 4162 delete mergedPlaylist.preloadSegment; 4163 } // if the update could overlap existing segment information, merge the two segment lists 4164 4165 4166 if (oldMedia.segments) { 4167 if (newMedia.skip) { 4168 newMedia.segments = newMedia.segments || []; // add back in objects for skipped segments, so that we merge 4169 // old properties into the new segments 4170 4171 for (let i = 0; i < newMedia.skip.skippedSegments; i++) { 4172 newMedia.segments.unshift({ 4173 skipped: true 4174 }); 4175 } 4176 } 4177 4178 mergedPlaylist.segments = updateSegments(oldMedia.segments, newMedia.segments, newMedia.mediaSequence - oldMedia.mediaSequence); 4179 } // resolve any segment URIs to prevent us from having to do it later 4180 4181 4182 mergedPlaylist.segments.forEach(segment => { 4183 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 4184 }); // TODO Right now in the playlists array there are two references to each playlist, one 4185 // that is referenced by index, and one by URI. The index reference may no longer be 4186 // necessary. 4187 4188 for (let i = 0; i < result.playlists.length; i++) { 4189 if (result.playlists[i].id === newMedia.id) { 4190 result.playlists[i] = mergedPlaylist; 4191 } 4192 } 4193 4194 result.playlists[newMedia.id] = mergedPlaylist; // URI reference added for backwards compatibility 4195 4196 result.playlists[newMedia.uri] = mergedPlaylist; // update media group playlist references. 4197 4198 forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => { 4199 if (!properties.playlists) { 4200 return; 4201 } 4202 4203 for (let i = 0; i < properties.playlists.length; i++) { 4204 if (newMedia.id === properties.playlists[i].id) { 4205 properties.playlists[i] = mergedPlaylist; 4206 } 4207 } 4208 }); 4209 return result; 4210 }; 4211 /** 4212 * Calculates the time to wait before refreshing a live playlist 4213 * 4214 * @param {Object} media 4215 * The current media 4216 * @param {boolean} update 4217 * True if there were any updates from the last refresh, false otherwise 4218 * @return {number} 4219 * The time in ms to wait before refreshing the live playlist 4220 */ 4221 4222 const refreshDelay = (media, update) => { 4223 const segments = media.segments || []; 4224 const lastSegment = segments[segments.length - 1]; 4225 const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1]; 4226 const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration; 4227 4228 if (update && lastDuration) { 4229 return lastDuration * 1000; 4230 } // if the playlist is unchanged since the last reload or last segment duration 4231 // cannot be determined, try again after half the target duration 4232 4233 4234 return (media.partTargetDuration || media.targetDuration || 10) * 500; 4235 }; 4236 /** 4237 * Load a playlist from a remote location 4238 * 4239 * @class PlaylistLoader 4240 * @extends Stream 4241 * @param {string|Object} src url or object of manifest 4242 * @param {boolean} withCredentials the withCredentials xhr option 4243 * @class 4244 */ 4245 4246 class PlaylistLoader extends EventTarget$1 { 4247 constructor(src, vhs, options = {}) { 4248 super(); 4249 4250 if (!src) { 4251 throw new Error('A non-empty playlist URL or object is required'); 4252 } 4253 4254 this.logger_ = logger('PlaylistLoader'); 4255 const { 4256 withCredentials = false 4257 } = options; 4258 this.src = src; 4259 this.vhs_ = vhs; 4260 this.withCredentials = withCredentials; 4261 this.addDateRangesToTextTrack_ = options.addDateRangesToTextTrack; 4262 const vhsOptions = vhs.options_; 4263 this.customTagParsers = vhsOptions && vhsOptions.customTagParsers || []; 4264 this.customTagMappers = vhsOptions && vhsOptions.customTagMappers || []; 4265 this.llhls = vhsOptions && vhsOptions.llhls; 4266 this.dateRangesStorage_ = new DateRangesStorage(); // initialize the loader state 4267 4268 this.state = 'HAVE_NOTHING'; // live playlist staleness timeout 4269 4270 this.handleMediaupdatetimeout_ = this.handleMediaupdatetimeout_.bind(this); 4271 this.on('mediaupdatetimeout', this.handleMediaupdatetimeout_); 4272 this.on('loadedplaylist', this.handleLoadedPlaylist_.bind(this)); 4273 } 4274 4275 handleLoadedPlaylist_() { 4276 const mediaPlaylist = this.media(); 4277 4278 if (!mediaPlaylist) { 4279 return; 4280 } 4281 4282 this.dateRangesStorage_.setOffset(mediaPlaylist.segments); 4283 this.dateRangesStorage_.setPendingDateRanges(mediaPlaylist.dateRanges); 4284 const availableDateRanges = this.dateRangesStorage_.getDateRangesToProcess(); 4285 4286 if (!availableDateRanges.length || !this.addDateRangesToTextTrack_) { 4287 return; 4288 } 4289 4290 this.addDateRangesToTextTrack_(availableDateRanges); 4291 } 4292 4293 handleMediaupdatetimeout_() { 4294 if (this.state !== 'HAVE_METADATA') { 4295 // only refresh the media playlist if no other activity is going on 4296 return; 4297 } 4298 4299 const media = this.media(); 4300 let uri = resolveUrl(this.main.uri, media.uri); 4301 4302 if (this.llhls) { 4303 uri = addLLHLSQueryDirectives(uri, media); 4304 } 4305 4306 this.state = 'HAVE_CURRENT_METADATA'; 4307 this.request = this.vhs_.xhr({ 4308 uri, 4309 withCredentials: this.withCredentials 4310 }, (error, req) => { 4311 // disposed 4312 if (!this.request) { 4313 return; 4314 } 4315 4316 if (error) { 4317 return this.playlistRequestError(this.request, this.media(), 'HAVE_METADATA'); 4318 } 4319 4320 this.haveMetadata({ 4321 playlistString: this.request.responseText, 4322 url: this.media().uri, 4323 id: this.media().id 4324 }); 4325 }); 4326 } 4327 4328 playlistRequestError(xhr, playlist, startingState) { 4329 const { 4330 uri, 4331 id 4332 } = playlist; // any in-flight request is now finished 4333 4334 this.request = null; 4335 4336 if (startingState) { 4337 this.state = startingState; 4338 } 4339 4340 this.error = { 4341 playlist: this.main.playlists[id], 4342 status: xhr.status, 4343 message: `HLS playlist request error at URL: ${uri}.`, 4344 responseText: xhr.responseText, 4345 code: xhr.status >= 500 ? 4 : 2 4346 }; 4347 this.trigger('error'); 4348 } 4349 4350 parseManifest_({ 4351 url, 4352 manifestString 4353 }) { 4354 return parseManifest({ 4355 onwarn: ({ 4356 message 4357 }) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), 4358 oninfo: ({ 4359 message 4360 }) => this.logger_(`m3u8-parser info for ${url}: ${message}`), 4361 manifestString, 4362 customTagParsers: this.customTagParsers, 4363 customTagMappers: this.customTagMappers, 4364 llhls: this.llhls 4365 }); 4366 } 4367 /** 4368 * Update the playlist loader's state in response to a new or updated playlist. 4369 * 4370 * @param {string} [playlistString] 4371 * Playlist string (if playlistObject is not provided) 4372 * @param {Object} [playlistObject] 4373 * Playlist object (if playlistString is not provided) 4374 * @param {string} url 4375 * URL of playlist 4376 * @param {string} id 4377 * ID to use for playlist 4378 */ 4379 4380 4381 haveMetadata({ 4382 playlistString, 4383 playlistObject, 4384 url, 4385 id 4386 }) { 4387 // any in-flight request is now finished 4388 this.request = null; 4389 this.state = 'HAVE_METADATA'; 4390 const playlist = playlistObject || this.parseManifest_({ 4391 url, 4392 manifestString: playlistString 4393 }); 4394 playlist.lastRequest = Date.now(); 4395 setupMediaPlaylist({ 4396 playlist, 4397 uri: url, 4398 id 4399 }); // merge this playlist into the main manifest 4400 4401 const update = updateMain$1(this.main, playlist); 4402 this.targetDuration = playlist.partTargetDuration || playlist.targetDuration; 4403 this.pendingMedia_ = null; 4404 4405 if (update) { 4406 this.main = update; 4407 this.media_ = this.main.playlists[id]; 4408 } else { 4409 this.trigger('playlistunchanged'); 4410 } 4411 4412 this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update)); 4413 this.trigger('loadedplaylist'); 4414 } 4415 /** 4416 * Abort any outstanding work and clean up. 4417 */ 4418 4419 4420 dispose() { 4421 this.trigger('dispose'); 4422 this.stopRequest(); 4423 window.clearTimeout(this.mediaUpdateTimeout); 4424 window.clearTimeout(this.finalRenditionTimeout); 4425 this.dateRangesStorage_ = new DateRangesStorage(); 4426 this.off(); 4427 } 4428 4429 stopRequest() { 4430 if (this.request) { 4431 const oldRequest = this.request; 4432 this.request = null; 4433 oldRequest.onreadystatechange = null; 4434 oldRequest.abort(); 4435 } 4436 } 4437 /** 4438 * When called without any arguments, returns the currently 4439 * active media playlist. When called with a single argument, 4440 * triggers the playlist loader to asynchronously switch to the 4441 * specified media playlist. Calling this method while the 4442 * loader is in the HAVE_NOTHING causes an error to be emitted 4443 * but otherwise has no effect. 4444 * 4445 * @param {Object=} playlist the parsed media playlist 4446 * object to switch to 4447 * @param {boolean=} shouldDelay whether we should delay the request by half target duration 4448 * 4449 * @return {Playlist} the current loaded media 4450 */ 4451 4452 4453 media(playlist, shouldDelay) { 4454 // getter 4455 if (!playlist) { 4456 return this.media_; 4457 } // setter 4458 4459 4460 if (this.state === 'HAVE_NOTHING') { 4461 throw new Error('Cannot switch media playlist from ' + this.state); 4462 } // find the playlist object if the target playlist has been 4463 // specified by URI 4464 4465 4466 if (typeof playlist === 'string') { 4467 if (!this.main.playlists[playlist]) { 4468 throw new Error('Unknown playlist URI: ' + playlist); 4469 } 4470 4471 playlist = this.main.playlists[playlist]; 4472 } 4473 4474 window.clearTimeout(this.finalRenditionTimeout); 4475 4476 if (shouldDelay) { 4477 const delay = (playlist.partTargetDuration || playlist.targetDuration) / 2 * 1000 || 5 * 1000; 4478 this.finalRenditionTimeout = window.setTimeout(this.media.bind(this, playlist, false), delay); 4479 return; 4480 } 4481 4482 const startingState = this.state; 4483 const mediaChange = !this.media_ || playlist.id !== this.media_.id; 4484 const mainPlaylistRef = this.main.playlists[playlist.id]; // switch to fully loaded playlists immediately 4485 4486 if (mainPlaylistRef && mainPlaylistRef.endList || // handle the case of a playlist object (e.g., if using vhs-json with a resolved 4487 // media playlist or, for the case of demuxed audio, a resolved audio media group) 4488 playlist.endList && playlist.segments.length) { 4489 // abort outstanding playlist requests 4490 if (this.request) { 4491 this.request.onreadystatechange = null; 4492 this.request.abort(); 4493 this.request = null; 4494 } 4495 4496 this.state = 'HAVE_METADATA'; 4497 this.media_ = playlist; // trigger media change if the active media has been updated 4498 4499 if (mediaChange) { 4500 this.trigger('mediachanging'); 4501 4502 if (startingState === 'HAVE_MAIN_MANIFEST') { 4503 // The initial playlist was a main manifest, and the first media selected was 4504 // also provided (in the form of a resolved playlist object) as part of the 4505 // source object (rather than just a URL). Therefore, since the media playlist 4506 // doesn't need to be requested, loadedmetadata won't trigger as part of the 4507 // normal flow, and needs an explicit trigger here. 4508 this.trigger('loadedmetadata'); 4509 } else { 4510 this.trigger('mediachange'); 4511 } 4512 } 4513 4514 return; 4515 } // We update/set the timeout here so that live playlists 4516 // that are not a media change will "start" the loader as expected. 4517 // We expect that this function will start the media update timeout 4518 // cycle again. This also prevents a playlist switch failure from 4519 // causing us to stall during live. 4520 4521 4522 this.updateMediaUpdateTimeout_(refreshDelay(playlist, true)); // switching to the active playlist is a no-op 4523 4524 if (!mediaChange) { 4525 return; 4526 } 4527 4528 this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request 4529 4530 if (this.request) { 4531 if (playlist.resolvedUri === this.request.url) { 4532 // requesting to switch to the same playlist multiple times 4533 // has no effect after the first 4534 return; 4535 } 4536 4537 this.request.onreadystatechange = null; 4538 this.request.abort(); 4539 this.request = null; 4540 } // request the new playlist 4541 4542 4543 if (this.media_) { 4544 this.trigger('mediachanging'); 4545 } 4546 4547 this.pendingMedia_ = playlist; 4548 this.request = this.vhs_.xhr({ 4549 uri: playlist.resolvedUri, 4550 withCredentials: this.withCredentials 4551 }, (error, req) => { 4552 // disposed 4553 if (!this.request) { 4554 return; 4555 } 4556 4557 playlist.lastRequest = Date.now(); 4558 playlist.resolvedUri = resolveManifestRedirect(playlist.resolvedUri, req); 4559 4560 if (error) { 4561 return this.playlistRequestError(this.request, playlist, startingState); 4562 } 4563 4564 this.haveMetadata({ 4565 playlistString: req.responseText, 4566 url: playlist.uri, 4567 id: playlist.id 4568 }); // fire loadedmetadata the first time a media playlist is loaded 4569 4570 if (startingState === 'HAVE_MAIN_MANIFEST') { 4571 this.trigger('loadedmetadata'); 4572 } else { 4573 this.trigger('mediachange'); 4574 } 4575 }); 4576 } 4577 /** 4578 * pause loading of the playlist 4579 */ 4580 4581 4582 pause() { 4583 if (this.mediaUpdateTimeout) { 4584 window.clearTimeout(this.mediaUpdateTimeout); 4585 this.mediaUpdateTimeout = null; 4586 } 4587 4588 this.stopRequest(); 4589 4590 if (this.state === 'HAVE_NOTHING') { 4591 // If we pause the loader before any data has been retrieved, its as if we never 4592 // started, so reset to an unstarted state. 4593 this.started = false; 4594 } // Need to restore state now that no activity is happening 4595 4596 4597 if (this.state === 'SWITCHING_MEDIA') { 4598 // if the loader was in the process of switching media, it should either return to 4599 // HAVE_MAIN_MANIFEST or HAVE_METADATA depending on if the loader has loaded a media 4600 // playlist yet. This is determined by the existence of loader.media_ 4601 if (this.media_) { 4602 this.state = 'HAVE_METADATA'; 4603 } else { 4604 this.state = 'HAVE_MAIN_MANIFEST'; 4605 } 4606 } else if (this.state === 'HAVE_CURRENT_METADATA') { 4607 this.state = 'HAVE_METADATA'; 4608 } 4609 } 4610 /** 4611 * start loading of the playlist 4612 */ 4613 4614 4615 load(shouldDelay) { 4616 if (this.mediaUpdateTimeout) { 4617 window.clearTimeout(this.mediaUpdateTimeout); 4618 this.mediaUpdateTimeout = null; 4619 } 4620 4621 const media = this.media(); 4622 4623 if (shouldDelay) { 4624 const delay = media ? (media.partTargetDuration || media.targetDuration) / 2 * 1000 : 5 * 1000; 4625 this.mediaUpdateTimeout = window.setTimeout(() => { 4626 this.mediaUpdateTimeout = null; 4627 this.load(); 4628 }, delay); 4629 return; 4630 } 4631 4632 if (!this.started) { 4633 this.start(); 4634 return; 4635 } 4636 4637 if (media && !media.endList) { 4638 this.trigger('mediaupdatetimeout'); 4639 } else { 4640 this.trigger('loadedplaylist'); 4641 } 4642 } 4643 4644 updateMediaUpdateTimeout_(delay) { 4645 if (this.mediaUpdateTimeout) { 4646 window.clearTimeout(this.mediaUpdateTimeout); 4647 this.mediaUpdateTimeout = null; 4648 } // we only have use mediaupdatetimeout for live playlists. 4649 4650 4651 if (!this.media() || this.media().endList) { 4652 return; 4653 } 4654 4655 this.mediaUpdateTimeout = window.setTimeout(() => { 4656 this.mediaUpdateTimeout = null; 4657 this.trigger('mediaupdatetimeout'); 4658 this.updateMediaUpdateTimeout_(delay); 4659 }, delay); 4660 } 4661 /** 4662 * start loading of the playlist 4663 */ 4664 4665 4666 start() { 4667 this.started = true; 4668 4669 if (typeof this.src === 'object') { 4670 // in the case of an entirely constructed manifest object (meaning there's no actual 4671 // manifest on a server), default the uri to the page's href 4672 if (!this.src.uri) { 4673 this.src.uri = window.location.href; 4674 } // resolvedUri is added on internally after the initial request. Since there's no 4675 // request for pre-resolved manifests, add on resolvedUri here. 4676 4677 4678 this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first 4679 // request can be skipped (since the top level of the manifest, at a minimum, is 4680 // already available as a parsed manifest object). However, if the manifest object 4681 // represents a main playlist, some media playlists may need to be resolved before 4682 // the starting segment list is available. Therefore, go directly to setup of the 4683 // initial playlist, and let the normal flow continue from there. 4684 // 4685 // Note that the call to setup is asynchronous, as other sections of VHS may assume 4686 // that the first request is asynchronous. 4687 4688 setTimeout(() => { 4689 this.setupInitialPlaylist(this.src); 4690 }, 0); 4691 return; 4692 } // request the specified URL 4693 4694 4695 this.request = this.vhs_.xhr({ 4696 uri: this.src, 4697 withCredentials: this.withCredentials 4698 }, (error, req) => { 4699 // disposed 4700 if (!this.request) { 4701 return; 4702 } // clear the loader's request reference 4703 4704 4705 this.request = null; 4706 4707 if (error) { 4708 this.error = { 4709 status: req.status, 4710 message: `HLS playlist request error at URL: ${this.src}.`, 4711 responseText: req.responseText, 4712 // MEDIA_ERR_NETWORK 4713 code: 2 4714 }; 4715 4716 if (this.state === 'HAVE_NOTHING') { 4717 this.started = false; 4718 } 4719 4720 return this.trigger('error'); 4721 } 4722 4723 this.src = resolveManifestRedirect(this.src, req); 4724 const manifest = this.parseManifest_({ 4725 manifestString: req.responseText, 4726 url: this.src
vendor: 12,249 bytes, lines 4726-5048
4726 4727 }); 4728 this.setupInitialPlaylist(manifest); 4729 }); 4730 } 4731 4732 srcUri() { 4733 return typeof this.src === 'string' ? this.src : this.src.uri; 4734 } 4735 /** 4736 * Given a manifest object that's either a main or media playlist, trigger the proper 4737 * events and set the state of the playlist loader. 4738 * 4739 * If the manifest object represents a main playlist, `loadedplaylist` will be 4740 * triggered to allow listeners to select a playlist. If none is selected, the loader 4741 * will default to the first one in the playlists array. 4742 * 4743 * If the manifest object represents a media playlist, `loadedplaylist` will be 4744 * triggered followed by `loadedmetadata`, as the only available playlist is loaded. 4745 * 4746 * In the case of a media playlist, a main playlist object wrapper with one playlist 4747 * will be created so that all logic can handle playlists in the same fashion (as an 4748 * assumed manifest object schema). 4749 * 4750 * @param {Object} manifest 4751 * The parsed manifest object 4752 */ 4753 4754 4755 setupInitialPlaylist(manifest) { 4756 this.state = 'HAVE_MAIN_MANIFEST'; 4757 4758 if (manifest.playlists) { 4759 this.main = manifest; 4760 addPropertiesToMain(this.main, this.srcUri()); // If the initial main playlist has playlists wtih segments already resolved, 4761 // then resolve URIs in advance, as they are usually done after a playlist request, 4762 // which may not happen if the playlist is resolved. 4763 4764 manifest.playlists.forEach(playlist => { 4765 playlist.segments = getAllSegments(playlist); 4766 playlist.segments.forEach(segment => { 4767 resolveSegmentUris(segment, playlist.resolvedUri); 4768 }); 4769 }); 4770 this.trigger('loadedplaylist'); 4771 4772 if (!this.request) { 4773 // no media playlist was specifically selected so start 4774 // from the first listed one 4775 this.media(this.main.playlists[0]); 4776 } 4777 4778 return; 4779 } // In order to support media playlists passed in as vhs-json, the case where the uri 4780 // is not provided as part of the manifest should be considered, and an appropriate 4781 // default used. 4782 4783 4784 const uri = this.srcUri() || window.location.href; 4785 this.main = mainForMedia(manifest, uri); 4786 this.haveMetadata({ 4787 playlistObject: manifest, 4788 url: uri, 4789 id: this.main.playlists[0].id 4790 }); 4791 this.trigger('loadedmetadata'); 4792 } 4793 /** 4794 * Updates or deletes a preexisting pathway clone. 4795 * Ensures that all playlists related to the old pathway clone are 4796 * either updated or deleted. 4797 * 4798 * @param {Object} clone On update, the pathway clone object for the newly updated pathway clone. 4799 * On delete, the old pathway clone object to be deleted. 4800 * @param {boolean} isUpdate True if the pathway is to be updated, 4801 * false if it is meant to be deleted. 4802 */ 4803 4804 4805 updateOrDeleteClone(clone, isUpdate) { 4806 const main = this.main; 4807 const pathway = clone.ID; 4808 let i = main.playlists.length; // Iterate backwards through the playlist so we can remove playlists if necessary. 4809 4810 while (i--) { 4811 const p = main.playlists[i]; 4812 4813 if (p.attributes['PATHWAY-ID'] === pathway) { 4814 const oldPlaylistUri = p.resolvedUri; 4815 const oldPlaylistId = p.id; // update the indexed playlist and add new playlists by ID and URI 4816 4817 if (isUpdate) { 4818 const newPlaylistUri = this.createCloneURI_(p.resolvedUri, clone); 4819 const newPlaylistId = createPlaylistID(pathway, newPlaylistUri); 4820 const attributes = this.createCloneAttributes_(pathway, p.attributes); 4821 const updatedPlaylist = this.createClonePlaylist_(p, newPlaylistId, clone, attributes); 4822 main.playlists[i] = updatedPlaylist; 4823 main.playlists[newPlaylistId] = updatedPlaylist; 4824 main.playlists[newPlaylistUri] = updatedPlaylist; 4825 } else { 4826 // Remove the indexed playlist. 4827 main.playlists.splice(i, 1); 4828 } // Remove playlists by the old ID and URI. 4829 4830 4831 delete main.playlists[oldPlaylistId]; 4832 delete main.playlists[oldPlaylistUri]; 4833 } 4834 } 4835 4836 this.updateOrDeleteCloneMedia(clone, isUpdate); 4837 } 4838 /** 4839 * Updates or deletes media data based on the pathway clone object. 4840 * Due to the complexity of the media groups and playlists, in all cases 4841 * we remove all of the old media groups and playlists. 4842 * On updates, we then create new media groups and playlists based on the 4843 * new pathway clone object. 4844 * 4845 * @param {Object} clone The pathway clone object for the newly updated pathway clone. 4846 * @param {boolean} isUpdate True if the pathway is to be updated, 4847 * false if it is meant to be deleted. 4848 */ 4849 4850 4851 updateOrDeleteCloneMedia(clone, isUpdate) { 4852 const main = this.main; 4853 const id = clone.ID; 4854 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 4855 if (!main.mediaGroups[mediaType] || !main.mediaGroups[mediaType][id]) { 4856 return; 4857 } 4858 4859 for (const groupKey in main.mediaGroups[mediaType]) { 4860 // Remove all media playlists for the media group for this pathway clone. 4861 if (groupKey === id) { 4862 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 4863 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 4864 oldMedia.playlists.forEach((p, i) => { 4865 const oldMediaPlaylist = main.playlists[p.id]; 4866 const oldPlaylistId = oldMediaPlaylist.id; 4867 const oldPlaylistUri = oldMediaPlaylist.resolvedUri; 4868 delete main.playlists[oldPlaylistId]; 4869 delete main.playlists[oldPlaylistUri]; 4870 }); 4871 } // Delete the old media group. 4872 4873 4874 delete main.mediaGroups[mediaType][groupKey]; 4875 } 4876 } 4877 }); // Create the new media groups and playlists if there is an update. 4878 4879 if (isUpdate) { 4880 this.createClonedMediaGroups_(clone); 4881 } 4882 } 4883 /** 4884 * Given a pathway clone object, clones all necessary playlists. 4885 * 4886 * @param {Object} clone The pathway clone object. 4887 * @param {Object} basePlaylist The original playlist to clone from. 4888 */ 4889 4890 4891 addClonePathway(clone, basePlaylist = {}) { 4892 const main = this.main; 4893 const index = main.playlists.length; 4894 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 4895 const playlistId = createPlaylistID(clone.ID, uri); 4896 const attributes = this.createCloneAttributes_(clone.ID, basePlaylist.attributes); 4897 const playlist = this.createClonePlaylist_(basePlaylist, playlistId, clone, attributes); 4898 main.playlists[index] = playlist; // add playlist by ID and URI 4899 4900 main.playlists[playlistId] = playlist; 4901 main.playlists[uri] = playlist; 4902 this.createClonedMediaGroups_(clone); 4903 } 4904 /** 4905 * Given a pathway clone object we create clones of all media. 4906 * In this function, all necessary information and updated playlists 4907 * are added to the `mediaGroup` object. 4908 * Playlists are also added to the `playlists` array so the media groups 4909 * will be properly linked. 4910 * 4911 * @param {Object} clone The pathway clone object. 4912 */ 4913 4914 4915 createClonedMediaGroups_(clone) { 4916 const id = clone.ID; 4917 const baseID = clone['BASE-ID']; 4918 const main = this.main; 4919 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 4920 // If the media type doesn't exist, or there is already a clone, skip 4921 // to the next media type. 4922 if (!main.mediaGroups[mediaType] || main.mediaGroups[mediaType][id]) { 4923 return; 4924 } 4925 4926 for (const groupKey in main.mediaGroups[mediaType]) { 4927 if (groupKey === baseID) { 4928 // Create the group. 4929 main.mediaGroups[mediaType][id] = {}; 4930 } else { 4931 // There is no need to iterate over label keys in this case. 4932 continue; 4933 } 4934 4935 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 4936 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 4937 main.mediaGroups[mediaType][id][labelKey] = _extends({}, oldMedia); 4938 const newMedia = main.mediaGroups[mediaType][id][labelKey]; // update URIs on the media 4939 4940 const newUri = this.createCloneURI_(oldMedia.resolvedUri, clone); 4941 newMedia.resolvedUri = newUri; 4942 newMedia.uri = newUri; // Reset playlists in the new media group. 4943 4944 newMedia.playlists = []; // Create new playlists in the newly cloned media group. 4945 4946 oldMedia.playlists.forEach((p, i) => { 4947 const oldMediaPlaylist = main.playlists[p.id]; 4948 const group = groupID(mediaType, id, labelKey); 4949 const newPlaylistID = createPlaylistID(id, group); // Check to see if it already exists 4950 4951 if (oldMediaPlaylist && !main.playlists[newPlaylistID]) { 4952 const newMediaPlaylist = this.createClonePlaylist_(oldMediaPlaylist, newPlaylistID, clone); 4953 const newPlaylistUri = newMediaPlaylist.resolvedUri; 4954 main.playlists[newPlaylistID] = newMediaPlaylist; 4955 main.playlists[newPlaylistUri] = newMediaPlaylist; 4956 } 4957 4958 newMedia.playlists[i] = this.createClonePlaylist_(p, newPlaylistID, clone); 4959 }); 4960 } 4961 } 4962 }); 4963 } 4964 /** 4965 * Using the original playlist to be cloned, and the pathway clone object 4966 * information, we create a new playlist. 4967 * 4968 * @param {Object} basePlaylist The original playlist to be cloned from. 4969 * @param {string} id The desired id of the newly cloned playlist. 4970 * @param {Object} clone The pathway clone object. 4971 * @param {Object} attributes An optional object to populate the `attributes` property in the playlist. 4972 * 4973 * @return {Object} The combined cloned playlist. 4974 */ 4975 4976 4977 createClonePlaylist_(basePlaylist, id, clone, attributes) { 4978 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 4979 const newProps = { 4980 resolvedUri: uri, 4981 uri, 4982 id 4983 }; // Remove all segments from previous playlist in the clone. 4984 4985 if (basePlaylist.segments) { 4986 newProps.segments = []; 4987 } 4988 4989 if (attributes) { 4990 newProps.attributes = attributes; 4991 } 4992 4993 return merge$1(basePlaylist, newProps); 4994 } 4995 /** 4996 * Generates an updated URI for a cloned pathway based on the original 4997 * pathway's URI and the paramaters from the pathway clone object in the 4998 * content steering server response. 4999 * 5000 * @param {string} baseUri URI to be updated in the cloned pathway. 5001 * @param {Object} clone The pathway clone object. 5002 * 5003 * @return {string} The updated URI for the cloned pathway. 5004 */ 5005 5006 5007 createCloneURI_(baseURI, clone) { 5008 const uri = new URL(baseURI); 5009 uri.hostname = clone['URI-REPLACEMENT'].HOST; 5010 const params = clone['URI-REPLACEMENT'].PARAMS; // Add params to the cloned URL. 5011 5012 for (const key of Object.keys(params)) { 5013 uri.searchParams.set(key, params[key]); 5014 } 5015 5016 return uri.href; 5017 } 5018 /** 5019 * Helper function to create the attributes needed for the new clone. 5020 * This mainly adds the necessary media attributes. 5021 * 5022 * @param {string} id The pathway clone object ID. 5023 * @param {Object} oldAttributes The old attributes to compare to. 5024 * @return {Object} The new attributes to add to the playlist. 5025 */ 5026 5027 5028 createCloneAttributes_(id, oldAttributes) { 5029 const attributes = { 5030 ['PATHWAY-ID']: id 5031 }; 5032 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 5033 if (oldAttributes[mediaType]) { 5034 attributes[mediaType] = id; 5035 } 5036 }); 5037 return attributes; 5038 } 5039 /** 5040 * Returns the key ID set from a playlist 5041 * 5042 * @param {playlist} playlist to fetch the key ID set from. 5043 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 5044 */ 5045 5046 5047 getKeyIdSet(playlist) { 5048
5048if (playlist.contentProtection) { 5049 const keyIds = new Set(); 5050 5051 for (const keysystem in playlist.contentProtection) { 5052 const keyId = playlist.contentProtection[keysystem].attributes.keyId; 5053 5054 if (keyId) { 5055 keyIds.add(keyId.toLowerCase()); 5056 } 5057 } 5058 5059 return keyIds; 5060 } 5061 } 5062 5063 } 5064 5065 /** 5066 * @file xhr.js 5067 */ 5068 const { 5069 xhr: videojsXHR 5070 } = videojs__default["default"]; 5071 5072 const callbackWrapper = function (request, error, response, callback) { 5073 const reqResponse = request.responseType === 'arraybuffer' ? request.response : request.responseText; 5074 5075 if (!error && reqResponse) { 5076 request.responseTime = Date.now(); 5077 request.roundTripTime = request.responseTime - request.requestTime; 5078 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 5079 5080 if (!request.bandwidth) { 5081 request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000); 5082 } 5083 } 5084 5085 if (response.headers) { 5086 request.responseHeaders = response.headers; 5087 } // videojs.xhr now uses a specific code on the error
vendor: 4,170 bytes, lines 5088-5188
5088 // object to signal that a request has timed out instead 5089 // of setting a boolean on the request object 5090 5091 5092 if (error && error.code === 'ETIMEDOUT') { 5093 request.timedout = true; 5094 } // videojs.xhr no longer considers status codes outside of 200 and 0 5095 // (for file uris) to be errors, but the old XHR did, so emulate that 5096 // behavior. Status 206 may be used in response to byterange requests. 5097 5098 5099 if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) { 5100 error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText))); 5101 } 5102 5103 callback(error, request); 5104 }; 5105 /** 5106 * Iterates over the request hooks Set and calls them in order 5107 * 5108 * @param {Set} hooks the hook Set to iterate over 5109 * @param {Object} options the request options to pass to the xhr wrapper 5110 * @return the callback hook function return value, the modified or new options Object. 5111 */ 5112 5113 5114 const callAllRequestHooks = (requestSet, options) => { 5115 if (!requestSet || !requestSet.size) { 5116 return; 5117 } 5118 5119 let newOptions = options; 5120 requestSet.forEach(requestCallback => { 5121 newOptions = requestCallback(newOptions); 5122 }); 5123 return newOptions; 5124 }; 5125 /** 5126 * Iterates over the response hooks Set and calls them in order. 5127 * 5128 * @param {Set} hooks the hook Set to iterate over 5129 * @param {Object} request the xhr request object 5130 * @param {Object} error the xhr error object 5131 * @param {Object} response the xhr response object 5132 */ 5133 5134 5135 const callAllResponseHooks = (responseSet, request, error, response) => { 5136 if (!responseSet || !responseSet.size) { 5137 return; 5138 } 5139 5140 responseSet.forEach(responseCallback => { 5141 responseCallback(request, error, response); 5142 }); 5143 }; 5144 5145 const xhrFactory = function () { 5146 const xhr = function XhrFunction(options, callback) { 5147 // Add a default timeout 5148 options = merge$1({ 5149 timeout: 45e3 5150 }, options); // Allow an optional user-specified function to modify the option 5151 // object before we construct the xhr request 5152 // TODO: Remove beforeRequest in the next major release. 5153 5154 const beforeRequest = XhrFunction.beforeRequest || videojs__default["default"].Vhs.xhr.beforeRequest; // onRequest and onResponse hooks as a Set, at either the player or global level. 5155 // TODO: new Set added here for beforeRequest alias. Remove this when beforeRequest is removed. 5156 5157 const _requestCallbackSet = XhrFunction._requestCallbackSet || videojs__default["default"].Vhs.xhr._requestCallbackSet || new Set(); 5158 5159 const _responseCallbackSet = XhrFunction._responseCallbackSet || videojs__default["default"].Vhs.xhr._responseCallbackSet; 5160 5161 if (beforeRequest && typeof beforeRequest === 'function') { 5162 videojs__default["default"].log.warn('beforeRequest is deprecated, use onRequest instead.'); 5163 5164 _requestCallbackSet.add(beforeRequest); 5165 } // Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden 5166 // TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11 5167 5168 5169 const xhrMethod = videojs__default["default"].Vhs.xhr.original === true ? videojsXHR : videojs__default["default"].Vhs.xhr; // call all registered onRequest hooks, assign new options. 5170 5171 const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options); // Remove the beforeRequest function from the hooks set so stale beforeRequest functions are not called. 5172 5173 _requestCallbackSet.delete(beforeRequest); // xhrMethod will call XMLHttpRequest.open and XMLHttpRequest.send 5174 5175 5176 const request = xhrMethod(beforeRequestOptions || options, function (error, response) { 5177 // call all registered onResponse hooks 5178 callAllResponseHooks(_responseCallbackSet, request, error, response); 5179 return callbackWrapper(request, error, response, callback); 5180 }); 5181 const originalAbort = request.abort; 5182 5183 request.abort = function () { 5184 request.aborted = true; 5185 return originalAbort.apply(request, arguments); 5186 }; 5187 5188 request.uri = options.uri
vendor: 26,398 bytes, lines 5188-6002
5188; 5189 request.requestTime = Date.now(); 5190 return request; 5191 }; 5192 5193 xhr.original = true; 5194 return xhr; 5195 }; 5196 /** 5197 * Turns segment byterange into a string suitable for use in 5198 * HTTP Range requests 5199 * 5200 * @param {Object} byterange - an object with two values defining the start and end 5201 * of a byte-range 5202 */ 5203 5204 5205 const byterangeStr = function (byterange) { 5206 // `byterangeEnd` is one less than `offset + length` because the HTTP range 5207 // header uses inclusive ranges 5208 let byterangeEnd; 5209 const byterangeStart = byterange.offset; 5210 5211 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 5212 byterangeEnd = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1); 5213 } else { 5214 byterangeEnd = byterange.offset + byterange.length - 1; 5215 } 5216 5217 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 5218 }; 5219 /** 5220 * Defines headers for use in the xhr request for a particular segment. 5221 * 5222 * @param {Object} segment - a simplified copy of the segmentInfo object 5223 * from SegmentLoader 5224 */ 5225 5226 const segmentXhrHeaders = function (segment) { 5227 const headers = {}; 5228 5229 if (segment.byterange) { 5230 headers.Range = byterangeStr(segment.byterange); 5231 } 5232 5233 return headers; 5234 }; 5235 5236 var MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; 5237 var DASH_REGEX = /^application\/dash\+xml/i; 5238 /** 5239 * Returns a string that describes the type of source based on a video source object's 5240 * media type. 5241 * 5242 * @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type} 5243 * 5244 * @param {string} type 5245 * Video source object media type 5246 * @return {('hls'|'dash'|'vhs-json'|null)} 5247 * VHS source type string 5248 */ 5249 5250 var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) { 5251 if (MPEGURL_REGEX.test(type)) { 5252 return 'hls'; 5253 } 5254 5255 if (DASH_REGEX.test(type)) { 5256 return 'dash'; 5257 } // Denotes the special case of a manifest object passed to http-streaming instead of a 5258 // source URL. 5259 // 5260 // See https://en.wikipedia.org/wiki/Media_type for details on specifying media types. 5261 // 5262 // In this case, vnd stands for vendor, video.js for the organization, VHS for this 5263 // project, and the +json suffix identifies the structure of the media type. 5264 5265 5266 if (type === 'application/vnd.videojs.vhs+json') { 5267 return 'vhs-json'; 5268 } 5269 5270 return null; 5271 }; 5272 5273 // const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2)); 5274 // we used to do this with log2 but BigInt does not support builtin math 5275 // Math.ceil(log2(x)); 5276 5277 5278 var countBits = function countBits(x) { 5279 return x.toString(2).length; 5280 }; // count the number of whole bytes it would take to represent a number 5281 5282 var countBytes = function countBytes(x) { 5283 return Math.ceil(countBits(x) / 8); 5284 }; 5285 var isArrayBufferView = function isArrayBufferView(obj) { 5286 if (ArrayBuffer.isView === 'function') { 5287 return ArrayBuffer.isView(obj); 5288 } 5289 5290 return obj && obj.buffer instanceof ArrayBuffer; 5291 }; 5292 var isTypedArray = function isTypedArray(obj) { 5293 return isArrayBufferView(obj); 5294 }; 5295 var toUint8 = function toUint8(bytes) { 5296 if (bytes instanceof Uint8Array) { 5297 return bytes; 5298 } 5299 5300 if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) { 5301 // any non-number or NaN leads to empty uint8array 5302 // eslint-disable-next-line 5303 if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) { 5304 bytes = 0; 5305 } else { 5306 bytes = [bytes]; 5307 } 5308 } 5309 5310 return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0); 5311 }; 5312 var BigInt = window.BigInt || Number; 5313 var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')]; 5314 (function () { 5315 var a = new Uint16Array([0xFFCC]); 5316 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 5317 5318 if (b[0] === 0xFF) { 5319 return 'big'; 5320 } 5321 5322 if (b[0] === 0xCC) { 5323 return 'little'; 5324 } 5325 5326 return 'unknown'; 5327 })(); 5328 var bytesToNumber = function bytesToNumber(bytes, _temp) { 5329 var _ref = _temp === void 0 ? {} : _temp, 5330 _ref$signed = _ref.signed, 5331 signed = _ref$signed === void 0 ? false : _ref$signed, 5332 _ref$le = _ref.le, 5333 le = _ref$le === void 0 ? false : _ref$le; 5334 5335 bytes = toUint8(bytes); 5336 var fn = le ? 'reduce' : 'reduceRight'; 5337 var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn]; 5338 var number = obj.call(bytes, function (total, byte, i) { 5339 var exponent = le ? i : Math.abs(i + 1 - bytes.length); 5340 return total + BigInt(byte) * BYTE_TABLE[exponent]; 5341 }, BigInt(0)); 5342 5343 if (signed) { 5344 var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1); 5345 number = BigInt(number); 5346 5347 if (number > max) { 5348 number -= max; 5349 number -= max; 5350 number -= BigInt(2); 5351 } 5352 } 5353 5354 return Number(number); 5355 }; 5356 var numberToBytes = function numberToBytes(number, _temp2) { 5357 var _ref2 = _temp2 === void 0 ? {} : _temp2, 5358 _ref2$le = _ref2.le, 5359 le = _ref2$le === void 0 ? false : _ref2$le; // eslint-disable-next-line 5360 5361 5362 if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) { 5363 number = 0; 5364 } 5365 5366 number = BigInt(number); 5367 var byteCount = countBytes(number); 5368 var bytes = new Uint8Array(new ArrayBuffer(byteCount)); 5369 5370 for (var i = 0; i < byteCount; i++) { 5371 var byteIndex = le ? i : Math.abs(i + 1 - bytes.length); 5372 bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF)); 5373 5374 if (number < 0) { 5375 bytes[byteIndex] = Math.abs(~bytes[byteIndex]); 5376 bytes[byteIndex] -= i === 0 ? 1 : 2; 5377 } 5378 } 5379 5380 return bytes; 5381 }; 5382 var stringToBytes = function stringToBytes(string, stringIsBytes) { 5383 if (typeof string !== 'string' && string && typeof string.toString === 'function') { 5384 string = string.toString(); 5385 } 5386 5387 if (typeof string !== 'string') { 5388 return new Uint8Array(); 5389 } // If the string already is bytes, we don't have to do this 5390 // otherwise we do this so that we split multi length characters 5391 // into individual bytes 5392 5393 5394 if (!stringIsBytes) { 5395 string = unescape(encodeURIComponent(string)); 5396 } 5397 5398 var view = new Uint8Array(string.length); 5399 5400 for (var i = 0; i < string.length; i++) { 5401 view[i] = string.charCodeAt(i); 5402 } 5403 5404 return view; 5405 }; 5406 var concatTypedArrays = function concatTypedArrays() { 5407 for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) { 5408 buffers[_key] = arguments[_key]; 5409 } 5410 5411 buffers = buffers.filter(function (b) { 5412 return b && (b.byteLength || b.length) && typeof b !== 'string'; 5413 }); 5414 5415 if (buffers.length <= 1) { 5416 // for 0 length we will return empty uint8 5417 // for 1 length we return the first uint8 5418 return toUint8(buffers[0]); 5419 } 5420 5421 var totalLen = buffers.reduce(function (total, buf, i) { 5422 return total + (buf.byteLength || buf.length); 5423 }, 0); 5424 var tempBuffer = new Uint8Array(totalLen); 5425 var offset = 0; 5426 buffers.forEach(function (buf) { 5427 buf = toUint8(buf); 5428 tempBuffer.set(buf, offset); 5429 offset += buf.byteLength; 5430 }); 5431 return tempBuffer; 5432 }; 5433 /** 5434 * Check if the bytes "b" are contained within bytes "a". 5435 * 5436 * @param {Uint8Array|Array} a 5437 * Bytes to check in 5438 * 5439 * @param {Uint8Array|Array} b 5440 * Bytes to check for 5441 * 5442 * @param {Object} options 5443 * options 5444 * 5445 * @param {Array|Uint8Array} [offset=0] 5446 * offset to use when looking at bytes in a 5447 * 5448 * @param {Array|Uint8Array} [mask=[]] 5449 * mask to use on bytes before comparison. 5450 * 5451 * @return {boolean} 5452 * If all bytes in b are inside of a, taking into account 5453 * bit masks. 5454 */ 5455 5456 var bytesMatch = function bytesMatch(a, b, _temp3) { 5457 var _ref3 = _temp3 === void 0 ? {} : _temp3, 5458 _ref3$offset = _ref3.offset, 5459 offset = _ref3$offset === void 0 ? 0 : _ref3$offset, 5460 _ref3$mask = _ref3.mask, 5461 mask = _ref3$mask === void 0 ? [] : _ref3$mask; 5462 5463 a = toUint8(a); 5464 b = toUint8(b); // ie 11 does not support uint8 every 5465 5466 var fn = b.every ? b.every : Array.prototype.every; 5467 return b.length && a.length - offset >= b.length && // ie 11 doesn't support every on uin8 5468 fn.call(b, function (bByte, i) { 5469 var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i]; 5470 return bByte === aByte; 5471 }); 5472 }; 5473 5474 /** 5475 * @file bin-utils.js 5476 */ 5477 5478 /** 5479 * convert a TimeRange to text 5480 * 5481 * @param {TimeRange} range the timerange to use for conversion 5482 * @param {number} i the iterator on the range to convert 5483 * @return {string} the range in string format 5484 */ 5485 5486 const textRange = function (range, i) { 5487 return range.start(i) + '-' + range.end(i); 5488 }; 5489 /** 5490 * format a number as hex string 5491 * 5492 * @param {number} e The number 5493 * @param {number} i the iterator 5494 * @return {string} the hex formatted number as a string 5495 */ 5496 5497 5498 const formatHexString = function (e, i) { 5499 const value = e.toString(16); 5500 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 5501 }; 5502 5503 const formatAsciiString = function (e) { 5504 if (e >= 0x20 && e < 0x7e) { 5505 return String.fromCharCode(e); 5506 } 5507 5508 return '.'; 5509 }; 5510 /** 5511 * Creates an object for sending to a web worker modifying properties that are TypedArrays 5512 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 5513 * 5514 * @param {Object} message 5515 * Object of properties and values to send to the web worker 5516 * @return {Object} 5517 * Modified message with TypedArray values expanded 5518 * @function createTransferableMessage 5519 */ 5520 5521 5522 const createTransferableMessage = function (message) { 5523 const transferable = {}; 5524 Object.keys(message).forEach(key => { 5525 const value = message[key]; 5526 5527 if (isArrayBufferView(value)) { 5528 transferable[key] = { 5529 bytes: value.buffer, 5530 byteOffset: value.byteOffset, 5531 byteLength: value.byteLength 5532 }; 5533 } else { 5534 transferable[key] = value; 5535 } 5536 }); 5537 return transferable; 5538 }; 5539 /** 5540 * Returns a unique string identifier for a media initialization 5541 * segment. 5542 * 5543 * @param {Object} initSegment 5544 * the init segment object. 5545 * 5546 * @return {string} the generated init segment id 5547 */ 5548 5549 const initSegmentId = function (initSegment) { 5550 const byterange = initSegment.byterange || { 5551 length: Infinity, 5552 offset: 0 5553 }; 5554 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 5555 }; 5556 /** 5557 * Returns a unique string identifier for a media segment key. 5558 * 5559 * @param {Object} key the encryption key 5560 * @return {string} the unique id for the media segment key. 5561 */ 5562 5563 const segmentKeyId = function (key) { 5564 return key.resolvedUri; 5565 }; 5566 /** 5567 * utils to help dump binary data to the console 5568 * 5569 * @param {Array|TypedArray} data 5570 * data to dump to a string 5571 * 5572 * @return {string} the data as a hex string. 5573 */ 5574 5575 const hexDump = data => { 5576 const bytes = Array.prototype.slice.call(data); 5577 const step = 16; 5578 let result = ''; 5579 let hex; 5580 let ascii; 5581 5582 for (let j = 0; j < bytes.length / step; j++) { 5583 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 5584 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 5585 result += hex + ' ' + ascii + '\n'; 5586 } 5587 5588 return result; 5589 }; 5590 const tagDump = ({ 5591 bytes 5592 }) => hexDump(bytes); 5593 const textRanges = ranges => { 5594 let result = ''; 5595 let i; 5596 5597 for (i = 0; i < ranges.length; i++) { 5598 result += textRange(ranges, i) + ' '; 5599 } 5600 5601 return result; 5602 }; 5603 5604 var utils = /*#__PURE__*/Object.freeze({ 5605 __proto__: null, 5606 createTransferableMessage: createTransferableMessage, 5607 initSegmentId: initSegmentId, 5608 segmentKeyId: segmentKeyId, 5609 hexDump: hexDump, 5610 tagDump: tagDump, 5611 textRanges: textRanges 5612 }); 5613 5614 // TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux 5615 // 25% was arbitrarily chosen, and may need to be refined over time. 5616 5617 const SEGMENT_END_FUDGE_PERCENT = 0.25; 5618 /** 5619 * Converts a player time (any time that can be gotten/set from player.currentTime(), 5620 * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a 5621 * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)). 5622 * 5623 * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor 5624 * point" (a point where we have a mapping from program time to player time, with player 5625 * time being the post transmux start of the segment). 5626 * 5627 * For more details, see [this doc](../../docs/program-time-from-player-time.md). 5628 * 5629 * @param {number} playerTime the player time 5630 * @param {Object} segment the segment which contains the player time 5631 * @return {Date} program time 5632 */ 5633 5634 const playerTimeToProgramTime = (playerTime, segment) => { 5635 if (!segment.dateTimeObject) { 5636 // Can't convert without an "anchor point" for the program time (i.e., a time that can 5637 // be used to map the start of a segment with a real world time). 5638 return null; 5639 } 5640 5641 const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds; 5642 const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended 5643 5644 const startOfSegment = transmuxedStart + transmuxerPrependedSeconds; 5645 const offsetFromSegmentStart = playerTime - startOfSegment; 5646 return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000); 5647 }; 5648 const originalSegmentVideoDuration = videoTimingInfo => { 5649 return videoTimingInfo.transmuxedPresentationEnd - videoTimingInfo.transmuxedPresentationStart - videoTimingInfo.transmuxerPrependedSeconds; 5650 }; 5651 /** 5652 * Finds a segment that contains the time requested given as an ISO-8601 string. The 5653 * returned segment might be an estimate or an accurate match. 5654 * 5655 * @param {string} programTime The ISO-8601 programTime to find a match for 5656 * @param {Object} playlist A playlist object to search within 5657 */ 5658 5659 const findSegmentForProgramTime = (programTime, playlist) => { 5660 // Assumptions: 5661 // - verifyProgramDateTimeTags has already been run 5662 // - live streams have been started 5663 let dateTimeObject; 5664 5665 try { 5666 dateTimeObject = new Date(programTime); 5667 } catch (e) { 5668 return null; 5669 } 5670 5671 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 5672 return null; 5673 } 5674 5675 let segment = playlist.segments[0]; 5676 5677 if (dateTimeObject < new Date(segment.dateTimeObject)) { 5678 // Requested time is before stream start. 5679 return null; 5680 } 5681 5682 for (let i = 0; i < playlist.segments.length - 1; i++) { 5683 segment = playlist.segments[i]; 5684 const nextSegmentStart = new Date(playlist.segments[i + 1].dateTimeObject); 5685 5686 if (dateTimeObject < nextSegmentStart) { 5687 break; 5688 } 5689 } 5690 5691 const lastSegment = playlist.segments[playlist.segments.length - 1]; 5692 const lastSegmentStart = lastSegment.dateTimeObject; 5693 const lastSegmentDuration = lastSegment.videoTimingInfo ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT; 5694 const lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000); 5695 5696 if (dateTimeObject > lastSegmentEnd) { 5697 // Beyond the end of the stream, or our best guess of the end of the stream. 5698 return null; 5699 } 5700 5701 if (dateTimeObject > new Date(lastSegmentStart)) { 5702 segment = lastSegment; 5703 } 5704 5705 return { 5706 segment, 5707 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)), 5708 // Although, given that all segments have accurate date time objects, the segment 5709 // selected should be accurate, unless the video has been transmuxed at some point 5710 // (determined by the presence of the videoTimingInfo object), the segment's "player 5711 // time" (the start time in the player) can't be considered accurate. 5712 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 5713 }; 5714 }; 5715 /** 5716 * Finds a segment that contains the given player time(in seconds). 5717 * 5718 * @param {number} time The player time to find a match for 5719 * @param {Object} playlist A playlist object to search within 5720 */ 5721 5722 const findSegmentForPlayerTime = (time, playlist) => { 5723 // Assumptions: 5724 // - there will always be a segment.duration 5725 // - we can start from zero 5726 // - segments are in time order 5727 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 5728 return null; 5729 } 5730 5731 let segmentEnd = 0; 5732 let segment; 5733 5734 for (let i = 0; i < playlist.segments.length; i++) { 5735 segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and 5736 // should contain the most accurate values we have for the segment's player times. 5737 // 5738 // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall 5739 // back to an estimate based on the manifest derived (inaccurate) segment.duration, to 5740 // calculate an end value. 5741 5742 segmentEnd = segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration; 5743 5744 if (time <= segmentEnd) { 5745 break; 5746 } 5747 } 5748 5749 const lastSegment = playlist.segments[playlist.segments.length - 1]; 5750 5751 if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) { 5752 // The time requested is beyond the stream end. 5753 return null; 5754 } 5755 5756 if (time > segmentEnd) { 5757 // The time is within or beyond the last segment. 5758 // 5759 // Check to see if the time is beyond a reasonable guess of the end of the stream. 5760 if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) { 5761 // Technically, because the duration value is only an estimate, the time may still 5762 // exist in the last segment, however, there isn't enough information to make even 5763 // a reasonable estimate. 5764 return null; 5765 } 5766 5767 segment = lastSegment; 5768 } 5769 5770 return { 5771 segment, 5772 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration, 5773 // Because videoTimingInfo is only set after transmux, it is the only way to get 5774 // accurate timing values. 5775 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 5776 }; 5777 }; 5778 /** 5779 * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds. 5780 * If the offset returned is positive, the programTime occurs after the 5781 * comparisonTimestamp. 5782 * If the offset is negative, the programTime occurs before the comparisonTimestamp. 5783 * 5784 * @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against 5785 * @param {string} programTime The programTime as an ISO-8601 string 5786 * @return {number} offset 5787 */ 5788 5789 const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => { 5790 let segmentDateTime; 5791 let programDateTime; 5792 5793 try { 5794 segmentDateTime = new Date(comparisonTimeStamp); 5795 programDateTime = new Date(programTime); 5796 } catch (e) {// TODO handle error 5797 } 5798 5799 const segmentTimeEpoch = segmentDateTime.getTime(); 5800 const programTimeEpoch = programDateTime.getTime(); 5801 return (programTimeEpoch - segmentTimeEpoch) / 1000; 5802 }; 5803 /** 5804 * Checks that all segments in this playlist have programDateTime tags. 5805 * 5806 * @param {Object} playlist A playlist object 5807 */ 5808 5809 const verifyProgramDateTimeTags = playlist => { 5810 if (!playlist.segments || playlist.segments.length === 0) { 5811 return false; 5812 } 5813 5814 for (let i = 0; i < playlist.segments.length; i++) { 5815 const segment = playlist.segments[i]; 5816 5817 if (!segment.dateTimeObject) { 5818 return false; 5819 } 5820 } 5821 5822 return true; 5823 }; 5824 /** 5825 * Returns the programTime of the media given a playlist and a playerTime. 5826 * The playlist must have programDateTime tags for a programDateTime tag to be returned. 5827 * If the segments containing the time requested have not been buffered yet, an estimate 5828 * may be returned to the callback. 5829 * 5830 * @param {Object} args 5831 * @param {Object} args.playlist A playlist object to search within 5832 * @param {number} time A playerTime in seconds 5833 * @param {Function} callback(err, programTime) 5834 * @return {string} err.message A detailed error message 5835 * @return {Object} programTime 5836 * @return {number} programTime.mediaSeconds The streamTime in seconds 5837 * @return {string} programTime.programDateTime The programTime as an ISO-8601 String 5838 */ 5839 5840 const getProgramTime = ({ 5841 playlist, 5842 time = undefined, 5843 callback 5844 }) => { 5845 if (!callback) { 5846 throw new Error('getProgramTime: callback must be provided'); 5847 } 5848 5849 if (!playlist || time === undefined) { 5850 return callback({ 5851 message: 'getProgramTime: playlist and time must be provided' 5852 }); 5853 } 5854 5855 const matchedSegment = findSegmentForPlayerTime(time, playlist); 5856 5857 if (!matchedSegment) { 5858 return callback({ 5859 message: 'valid programTime was not found' 5860 }); 5861 } 5862 5863 if (matchedSegment.type === 'estimate') { 5864 return callback({ 5865 message: 'Accurate programTime could not be determined.' + ' Please seek to e.seekTime and try again', 5866 seekTime: matchedSegment.estimatedStart 5867 }); 5868 } 5869 5870 const programTimeObject = { 5871 mediaSeconds: time 5872 }; 5873 const programTime = playerTimeToProgramTime(time, matchedSegment.segment); 5874 5875 if (programTime) { 5876 programTimeObject.programDateTime = programTime.toISOString(); 5877 } 5878 5879 return callback(null, programTimeObject); 5880 }; 5881 /** 5882 * Seeks in the player to a time that matches the given programTime ISO-8601 string. 5883 * 5884 * @param {Object} args 5885 * @param {string} args.programTime A programTime to seek to as an ISO-8601 String 5886 * @param {Object} args.playlist A playlist to look within 5887 * @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2. 5888 * @param {Function} args.seekTo A method to perform a seek 5889 * @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true. 5890 * @param {Object} args.tech The tech to seek on 5891 * @param {Function} args.callback(err, newTime) A callback to return the new time to 5892 * @return {string} err.message A detailed error message 5893 * @return {number} newTime The exact time that was seeked to in seconds 5894 */ 5895 5896 const seekToProgramTime = ({ 5897 programTime, 5898 playlist, 5899 retryCount = 2, 5900 seekTo, 5901 pauseAfterSeek = true, 5902 tech, 5903 callback 5904 }) => { 5905 if (!callback) { 5906 throw new Error('seekToProgramTime: callback must be provided'); 5907 } 5908 5909 if (typeof programTime === 'undefined' || !playlist || !seekTo) { 5910 return callback({ 5911 message: 'seekToProgramTime: programTime, seekTo and playlist must be provided' 5912 }); 5913 } 5914 5915 if (!playlist.endList && !tech.hasStarted_) { 5916 return callback({ 5917 message: 'player must be playing a live stream to start buffering' 5918 }); 5919 } 5920 5921 if (!verifyProgramDateTimeTags(playlist)) { 5922 return callback({ 5923 message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri 5924 }); 5925 } 5926 5927 const matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match 5928 5929 if (!matchedSegment) { 5930 return callback({ 5931 message: `${programTime} was not found in the stream` 5932 }); 5933 } 5934 5935 const segment = matchedSegment.segment; 5936 const mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime); 5937 5938 if (matchedSegment.type === 'estimate') { 5939 // we've run out of retries 5940 if (retryCount === 0) { 5941 return callback({ 5942 message: `${programTime} is not buffered yet. Try again` 5943 }); 5944 } 5945 5946 seekTo(matchedSegment.estimatedStart + mediaOffset); 5947 tech.one('seeked', () => { 5948 seekToProgramTime({ 5949 programTime, 5950 playlist, 5951 retryCount: retryCount - 1, 5952 seekTo, 5953 pauseAfterSeek, 5954 tech, 5955 callback 5956 }); 5957 }); 5958 return; 5959 } // Since the segment.start value is determined from the buffered end or ending time 5960 // of the prior segment, the seekToTime doesn't need to account for any transmuxer 5961 // modifications. 5962 5963 5964 const seekToTime = segment.start + mediaOffset; 5965 5966 const seekedCallback = () => { 5967 return callback(null, tech.currentTime()); 5968 }; // listen for seeked event 5969 5970 5971 tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state 5972 5973 if (pauseAfterSeek) { 5974 tech.pause(); 5975 } 5976 5977 seekTo(seekToTime); 5978 }; 5979 5980 /** 5981 * Loops through all supported media groups in master and calls the provided 5982 * callback for each group 5983 * 5984 * @param {Object} master 5985 * The parsed master manifest object 5986 * @param {string[]} groups 5987 * The media groups to call the callback for 5988 * @param {Function} callback 5989 * Callback to call for each media group 5990 */ 5991 var forEachMediaGroup = function forEachMediaGroup(master, groups, callback) { 5992 groups.forEach(function (mediaType) { 5993 for (var groupKey in master.mediaGroups[mediaType]) { 5994 for (var labelKey in master.mediaGroups[mediaType][groupKey]) { 5995 var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey]; 5996 callback(mediaProperties, mediaType, groupKey, labelKey); 5997 } 5998 } 5999 }); 6000 }; 6001 6002 /*! @name mpd-parser @version 1.
60023
vendor: 33,195 bytes, lines 6002-6976
6002.0 @license Apache-2.0 */ 6003 6004 const isObject = obj => { 6005 return !!obj && typeof obj === 'object'; 6006 }; 6007 6008 const merge = (...objects) => { 6009 return objects.reduce((result, source) => { 6010 if (typeof source !== 'object') { 6011 return result; 6012 } 6013 6014 Object.keys(source).forEach(key => { 6015 if (Array.isArray(result[key]) && Array.isArray(source[key])) { 6016 result[key] = result[key].concat(source[key]); 6017 } else if (isObject(result[key]) && isObject(source[key])) { 6018 result[key] = merge(result[key], source[key]); 6019 } else { 6020 result[key] = source[key]; 6021 } 6022 }); 6023 return result; 6024 }, {}); 6025 }; 6026 6027 const values = o => Object.keys(o).map(k => o[k]); 6028 6029 const range = (start, end) => { 6030 const result = []; 6031 6032 for (let i = start; i < end; i++) { 6033 result.push(i); 6034 } 6035 6036 return result; 6037 }; 6038 6039 const flatten = lists => lists.reduce((x, y) => x.concat(y), []); 6040 6041 const from = list => { 6042 if (!list.length) { 6043 return []; 6044 } 6045 6046 const result = []; 6047 6048 for (let i = 0; i < list.length; i++) { 6049 result.push(list[i]); 6050 } 6051 6052 return result; 6053 }; 6054 6055 const findIndexes = (l, key) => l.reduce((a, e, i) => { 6056 if (e[key]) { 6057 a.push(i); 6058 } 6059 6060 return a; 6061 }, []); 6062 /** 6063 * Returns a union of the included lists provided each element can be identified by a key. 6064 * 6065 * @param {Array} list - list of lists to get the union of 6066 * @param {Function} keyFunction - the function to use as a key for each element 6067 * 6068 * @return {Array} the union of the arrays 6069 */ 6070 6071 6072 const union = (lists, keyFunction) => { 6073 return values(lists.reduce((acc, list) => { 6074 list.forEach(el => { 6075 acc[keyFunction(el)] = el; 6076 }); 6077 return acc; 6078 }, {})); 6079 }; 6080 6081 var errors = { 6082 INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', 6083 INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING', 6084 DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', 6085 DASH_INVALID_XML: 'DASH_INVALID_XML', 6086 NO_BASE_URL: 'NO_BASE_URL', 6087 MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', 6088 SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', 6089 UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' 6090 }; 6091 /** 6092 * @typedef {Object} SingleUri 6093 * @property {string} uri - relative location of segment 6094 * @property {string} resolvedUri - resolved location of segment 6095 * @property {Object} byterange - Object containing information on how to make byte range 6096 * requests following byte-range-spec per RFC2616. 6097 * @property {String} byterange.length - length of range request 6098 * @property {String} byterange.offset - byte offset of range request 6099 * 6100 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 6101 */ 6102 6103 /** 6104 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object 6105 * that conforms to how m3u8-parser is structured 6106 * 6107 * @see https://github.com/videojs/m3u8-parser 6108 * 6109 * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes 6110 * @param {string} source - source url for segment 6111 * @param {string} range - optional range used for range calls, 6112 * follows RFC 2616, Clause 14.35.1 6113 * @return {SingleUri} full segment information transformed into a format similar 6114 * to m3u8-parser 6115 */ 6116 6117 const urlTypeToSegment = ({ 6118 baseUrl = '', 6119 source = '', 6120 range = '', 6121 indexRange = '' 6122 }) => { 6123 const segment = { 6124 uri: source, 6125 resolvedUri: resolveUrl$1(baseUrl || '', source) 6126 }; 6127 6128 if (range || indexRange) { 6129 const rangeStr = range ? range : indexRange; 6130 const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible 6131 6132 let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10); 6133 let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER 6134 6135 if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') { 6136 startRange = Number(startRange); 6137 } 6138 6139 if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') { 6140 endRange = Number(endRange); 6141 } 6142 6143 let length; 6144 6145 if (typeof endRange === 'bigint' || typeof startRange === 'bigint') { 6146 length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1); 6147 } else { 6148 length = endRange - startRange + 1; 6149 } 6150 6151 if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) { 6152 length = Number(length); 6153 } // byterange should be inclusive according to 6154 // RFC 2616, Clause 14.35.1 6155 6156 6157 segment.byterange = { 6158 length, 6159 offset: startRange 6160 }; 6161 } 6162 6163 return segment; 6164 }; 6165 6166 const byteRangeToString = byterange => { 6167 // `endRange` is one less than `offset + length` because the HTTP range 6168 // header uses inclusive ranges 6169 let endRange; 6170 6171 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 6172 endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1); 6173 } else { 6174 endRange = byterange.offset + byterange.length - 1; 6175 } 6176 6177 return `${byterange.offset}-${endRange}`; 6178 }; 6179 /** 6180 * parse the end number attribue that can be a string 6181 * number, or undefined. 6182 * 6183 * @param {string|number|undefined} endNumber 6184 * The end number attribute. 6185 * 6186 * @return {number|null} 6187 * The result of parsing the end number. 6188 */ 6189 6190 6191 const parseEndNumber = endNumber => { 6192 if (endNumber && typeof endNumber !== 'number') { 6193 endNumber = parseInt(endNumber, 10); 6194 } 6195 6196 if (isNaN(endNumber)) { 6197 return null; 6198 } 6199 6200 return endNumber; 6201 }; 6202 /** 6203 * Functions for calculating the range of available segments in static and dynamic 6204 * manifests. 6205 */ 6206 6207 6208 const segmentRange = { 6209 /** 6210 * Returns the entire range of available segments for a static MPD 6211 * 6212 * @param {Object} attributes 6213 * Inheritied MPD attributes 6214 * @return {{ start: number, end: number }} 6215 * The start and end numbers for available segments 6216 */ 6217 static(attributes) { 6218 const { 6219 duration, 6220 timescale = 1, 6221 sourceDuration, 6222 periodDuration 6223 } = attributes; 6224 const endNumber = parseEndNumber(attributes.endNumber); 6225 const segmentDuration = duration / timescale; 6226 6227 if (typeof endNumber === 'number') { 6228 return { 6229 start: 0, 6230 end: endNumber 6231 }; 6232 } 6233 6234 if (typeof periodDuration === 'number') { 6235 return { 6236 start: 0, 6237 end: periodDuration / segmentDuration 6238 }; 6239 } 6240 6241 return { 6242 start: 0, 6243 end: sourceDuration / segmentDuration 6244 }; 6245 }, 6246 6247 /** 6248 * Returns the current live window range of available segments for a dynamic MPD 6249 * 6250 * @param {Object} attributes 6251 * Inheritied MPD attributes 6252 * @return {{ start: number, end: number }} 6253 * The start and end numbers for available segments 6254 */ 6255 dynamic(attributes) { 6256 const { 6257 NOW, 6258 clientOffset, 6259 availabilityStartTime, 6260 timescale = 1, 6261 duration, 6262 periodStart = 0, 6263 minimumUpdatePeriod = 0, 6264 timeShiftBufferDepth = Infinity 6265 } = attributes; 6266 const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated 6267 // after retrieving UTC server time. 6268 6269 const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock. 6270 // Convert the period start time to EPOCH. 6271 6272 const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update. 6273 6274 const periodEndWC = now + minimumUpdatePeriod; 6275 const periodDuration = periodEndWC - periodStartWC; 6276 const segmentCount = Math.ceil(periodDuration * timescale / duration); 6277 const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); 6278 const availableEnd = Math.floor((now - periodStartWC) * timescale / duration); 6279 return { 6280 start: Math.max(0, availableStart), 6281 end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd) 6282 }; 6283 } 6284 6285 }; 6286 /** 6287 * Maps a range of numbers to objects with information needed to build the corresponding 6288 * segment list 6289 * 6290 * @name toSegmentsCallback 6291 * @function 6292 * @param {number} number 6293 * Number of the segment 6294 * @param {number} index 6295 * Index of the number in the range list 6296 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} 6297 * Object with segment timing and duration info 6298 */ 6299 6300 /** 6301 * Returns a callback for Array.prototype.map for mapping a range of numbers to 6302 * information needed to build the segment list. 6303 * 6304 * @param {Object} attributes 6305 * Inherited MPD attributes 6306 * @return {toSegmentsCallback} 6307 * Callback map function 6308 */ 6309 6310 const toSegments = attributes => number => { 6311 const { 6312 duration, 6313 timescale = 1, 6314 periodStart, 6315 startNumber = 1 6316 } = attributes; 6317 return { 6318 number: startNumber + number, 6319 duration: duration / timescale, 6320 timeline: periodStart, 6321 time: number * duration 6322 }; 6323 }; 6324 /** 6325 * Returns a list of objects containing segment timing and duration info used for 6326 * building the list of segments. This uses the @duration attribute specified 6327 * in the MPD manifest to derive the range of segments. 6328 * 6329 * @param {Object} attributes 6330 * Inherited MPD attributes 6331 * @return {{number: number, duration: number, time: number, timeline: number}[]} 6332 * List of Objects with segment timing and duration info 6333 */ 6334 6335 6336 const parseByDuration = attributes => { 6337 const { 6338 type, 6339 duration, 6340 timescale = 1, 6341 periodDuration, 6342 sourceDuration 6343 } = attributes; 6344 const { 6345 start, 6346 end 6347 } = segmentRange[type](attributes); 6348 const segments = range(start, end).map(toSegments(attributes)); 6349 6350 if (type === 'static') { 6351 const index = segments.length - 1; // section is either a period or the full source 6352 6353 const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration 6354 6355 segments[index].duration = sectionDuration - duration / timescale * index; 6356 } 6357 6358 return segments; 6359 }; 6360 /** 6361 * Translates SegmentBase into a set of segments. 6362 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 6363 * node should be translated into segment. 6364 * 6365 * @param {Object} attributes 6366 * Object containing all inherited attributes from parent elements with attribute 6367 * names as keys 6368 * @return {Object.<Array>} list of segments 6369 */ 6370 6371 6372 const segmentsFromBase = attributes => { 6373 const { 6374 baseUrl, 6375 initialization = {}, 6376 sourceDuration, 6377 indexRange = '', 6378 periodStart, 6379 presentationTime, 6380 number = 0, 6381 duration 6382 } = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) 6383 6384 if (!baseUrl) { 6385 throw new Error(errors.NO_BASE_URL); 6386 } 6387 6388 const initSegment = urlTypeToSegment({ 6389 baseUrl, 6390 source: initialization.sourceURL, 6391 range: initialization.range 6392 }); 6393 const segment = urlTypeToSegment({ 6394 baseUrl, 6395 source: baseUrl, 6396 indexRange 6397 }); 6398 segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source 6399 // (since SegmentBase is only for one total segment) 6400 6401 if (duration) { 6402 const segmentTimeInfo = parseByDuration(attributes); 6403 6404 if (segmentTimeInfo.length) { 6405 segment.duration = segmentTimeInfo[0].duration; 6406 segment.timeline = segmentTimeInfo[0].timeline; 6407 } 6408 } else if (sourceDuration) { 6409 segment.duration = sourceDuration; 6410 segment.timeline = periodStart; 6411 } // If presentation time is provided, these segments are being generated by SIDX 6412 // references, and should use the time provided. For the general case of SegmentBase, 6413 // there should only be one segment in the period, so its presentation time is the same 6414 // as its period start. 6415 6416 6417 segment.presentationTime = presentationTime || periodStart; 6418 segment.number = number; 6419 return [segment]; 6420 }; 6421 /** 6422 * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist 6423 * according to the sidx information given. 6424 * 6425 * playlist.sidx has metadadata about the sidx where-as the sidx param 6426 * is the parsed sidx box itself. 6427 * 6428 * @param {Object} playlist the playlist to update the sidx information for 6429 * @param {Object} sidx the parsed sidx box 6430 * @return {Object} the playlist object with the updated sidx information 6431 */ 6432 6433 6434 const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => { 6435 // Retain init segment information 6436 const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing 6437 6438 const sourceDuration = playlist.sidx.duration; // Retain source timeline 6439 6440 const timeline = playlist.timeline || 0; 6441 const sidxByteRange = playlist.sidx.byterange; 6442 const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx 6443 6444 const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes 6445 6446 const mediaReferences = sidx.references.filter(r => r.referenceType !== 1); 6447 const segments = []; 6448 const type = playlist.endList ? 'static' : 'dynamic'; 6449 const periodStart = playlist.sidx.timeline; 6450 let presentationTime = periodStart; 6451 let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box 6452 6453 let startIndex; // eslint-disable-next-line 6454 6455 if (typeof sidx.firstOffset === 'bigint') { 6456 startIndex = window.BigInt(sidxEnd) + sidx.firstOffset; 6457 } else { 6458 startIndex = sidxEnd + sidx.firstOffset; 6459 } 6460 6461 for (let i = 0; i < mediaReferences.length; i++) { 6462 const reference = sidx.references[i]; // size of the referenced (sub)segment 6463 6464 const size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale 6465 // this will be converted to seconds when generating segments 6466 6467 const duration = reference.subsegmentDuration; // should be an inclusive range 6468 6469 let endIndex; // eslint-disable-next-line 6470 6471 if (typeof startIndex === 'bigint') { 6472 endIndex = startIndex + window.BigInt(size) - window.BigInt(1); 6473 } else { 6474 endIndex = startIndex + size - 1; 6475 } 6476 6477 const indexRange = `${startIndex}-${endIndex}`; 6478 const attributes = { 6479 baseUrl, 6480 timescale, 6481 timeline, 6482 periodStart, 6483 presentationTime, 6484 number, 6485 duration, 6486 sourceDuration, 6487 indexRange, 6488 type 6489 }; 6490 const segment = segmentsFromBase(attributes)[0]; 6491 6492 if (initSegment) { 6493 segment.map = initSegment; 6494 } 6495 6496 segments.push(segment); 6497 6498 if (typeof startIndex === 'bigint') { 6499 startIndex += window.BigInt(size); 6500 } else { 6501 startIndex += size; 6502 } 6503 6504 presentationTime += duration / timescale; 6505 number++; 6506 } 6507 6508 playlist.segments = segments; 6509 return playlist; 6510 }; 6511 6512 const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen) 6513 6514 const TIME_FUDGE = 1 / 60; 6515 /** 6516 * Given a list of timelineStarts, combines, dedupes, and sorts them. 6517 * 6518 * @param {TimelineStart[]} timelineStarts - list of timeline starts 6519 * 6520 * @return {TimelineStart[]} the combined and deduped timeline starts 6521 */ 6522 6523 const getUniqueTimelineStarts = timelineStarts => { 6524 return union(timelineStarts, ({ 6525 timeline 6526 }) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1); 6527 }; 6528 /** 6529 * Finds the playlist with the matching NAME attribute. 6530 * 6531 * @param {Array} playlists - playlists to search through 6532 * @param {string} name - the NAME attribute to search for 6533 * 6534 * @return {Object|null} the matching playlist object, or null 6535 */ 6536 6537 6538 const findPlaylistWithName = (playlists, name) => { 6539 for (let i = 0; i < playlists.length; i++) { 6540 if (playlists[i].attributes.NAME === name) { 6541 return playlists[i]; 6542 } 6543 } 6544 6545 return null; 6546 }; 6547 /** 6548 * Gets a flattened array of media group playlists. 6549 * 6550 * @param {Object} manifest - the main manifest object 6551 * 6552 * @return {Array} the media group playlists 6553 */ 6554 6555 6556 const getMediaGroupPlaylists = manifest => { 6557 let mediaGroupPlaylists = []; 6558 forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => { 6559 mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []); 6560 }); 6561 return mediaGroupPlaylists; 6562 }; 6563 /** 6564 * Updates the playlist's media sequence numbers. 6565 * 6566 * @param {Object} config - options object 6567 * @param {Object} config.playlist - the playlist to update 6568 * @param {number} config.mediaSequence - the mediaSequence number to start with 6569 */ 6570 6571 6572 const updateMediaSequenceForPlaylist = ({ 6573 playlist, 6574 mediaSequence 6575 }) => { 6576 playlist.mediaSequence = mediaSequence; 6577 playlist.segments.forEach((segment, index) => { 6578 segment.number = playlist.mediaSequence + index; 6579 }); 6580 }; 6581 /** 6582 * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists 6583 * and a complete list of timeline starts. 6584 * 6585 * If no matching playlist is found, only the discontinuity sequence number of the playlist 6586 * will be updated. 6587 * 6588 * Since early available timelines are not supported, at least one segment must be present. 6589 * 6590 * @param {Object} config - options object 6591 * @param {Object[]} oldPlaylists - the old playlists to use as a reference 6592 * @param {Object[]} newPlaylists - the new playlists to update 6593 * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point 6594 */ 6595 6596 6597 const updateSequenceNumbers = ({ 6598 oldPlaylists, 6599 newPlaylists, 6600 timelineStarts 6601 }) => { 6602 newPlaylists.forEach(playlist => { 6603 playlist.discontinuitySequence = timelineStarts.findIndex(function ({ 6604 timeline 6605 }) { 6606 return timeline === playlist.timeline; 6607 }); // Playlists NAMEs come from DASH Representation IDs, which are mandatory 6608 // (see ISO_23009-1-2012 5.3.5.2). 6609 // 6610 // If the same Representation existed in a prior Period, it will retain the same NAME. 6611 6612 const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME); 6613 6614 if (!oldPlaylist) { 6615 // Since this is a new playlist, the media sequence values can start from 0 without 6616 // consequence. 6617 return; 6618 } // TODO better support for live SIDX 6619 // 6620 // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD). 6621 // This is evident by a playlist only having a single SIDX reference. In a multiperiod 6622 // playlist there would need to be multiple SIDX references. In addition, live SIDX is 6623 // not supported when the SIDX properties change on refreshes. 6624 // 6625 // In the future, if support needs to be added, the merging logic here can be called 6626 // after SIDX references are resolved. For now, exit early to prevent exceptions being 6627 // thrown due to undefined references. 6628 6629 6630 if (playlist.sidx) { 6631 return; 6632 } // Since we don't yet support early available timelines, we don't need to support 6633 // playlists with no segments. 6634 6635 6636 const firstNewSegment = playlist.segments[0]; 6637 const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) { 6638 return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE; 6639 }); // No matching segment from the old playlist means the entire playlist was refreshed. 6640 // In this case the media sequence should account for this update, and the new segments 6641 // should be marked as discontinuous from the prior content, since the last prior 6642 // timeline was removed. 6643 6644 if (oldMatchingSegmentIndex === -1) { 6645 updateMediaSequenceForPlaylist({ 6646 playlist, 6647 mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length 6648 }); 6649 playlist.segments[0].discontinuity = true; 6650 playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content. 6651 // 6652 // If the new playlist's timeline is the same as the last seen segment's timeline, 6653 // then a discontinuity can be added to identify that there's potentially missing 6654 // content. If there's no missing content, the discontinuity should still be rather 6655 // harmless. It's possible that if segment durations are accurate enough, that the 6656 // existence of a gap can be determined using the presentation times and durations, 6657 // but if the segment timing info is off, it may introduce more problems than simply 6658 // adding the discontinuity. 6659 // 6660 // If the new playlist's timeline is different from the last seen segment's timeline, 6661 // then a discontinuity can be added to identify that this is the first seen segment 6662 // of a new timeline. However, the logic at the start of this function that 6663 // determined the disconinuity sequence by timeline index is now off by one (the 6664 // discontinuity of the newest timeline hasn't yet fallen off the manifest...since 6665 // we added it), so the disconinuity sequence must be decremented. 6666 // 6667 // A period may also have a duration of zero, so the case of no segments is handled 6668 // here even though we don't yet support early available periods. 6669 6670 if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) { 6671 playlist.discontinuitySequence--; 6672 } 6673 6674 return; 6675 } // If the first segment matched with a prior segment on a discontinuity (it's matching 6676 // on the first segment of a period), then the discontinuitySequence shouldn't be the 6677 // timeline's matching one, but instead should be the one prior, and the first segment 6678 // of the new manifest should be marked with a discontinuity. 6679 // 6680 // The reason for this special case is that discontinuity sequence shows how many 6681 // discontinuities have fallen off of the playlist, and discontinuities are marked on 6682 // the first segment of a new "timeline." Because of this, while DASH will retain that 6683 // Period while the "timeline" exists, HLS keeps track of it via the discontinuity 6684 // sequence, and that first segment is an indicator, but can be removed before that 6685 // timeline is gone. 6686 6687 6688 const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex]; 6689 6690 if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) { 6691 firstNewSegment.discontinuity = true; 6692 playlist.discontinuityStarts.unshift(0); 6693 playlist.discontinuitySequence--; 6694 } 6695 6696 updateMediaSequenceForPlaylist({ 6697 playlist, 6698 mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number 6699 }); 6700 }); 6701 }; 6702 /** 6703 * Given an old parsed manifest object and a new parsed manifest object, updates the 6704 * sequence and timing values within the new manifest to ensure that it lines up with the 6705 * old. 6706 * 6707 * @param {Array} oldManifest - the old main manifest object 6708 * @param {Array} newManifest - the new main manifest object 6709 * 6710 * @return {Object} the updated new manifest object 6711 */ 6712 6713 6714 const positionManifestOnTimeline = ({ 6715 oldManifest, 6716 newManifest 6717 }) => { 6718 // Starting from v4.1.2 of the IOP, section 4.4.3.3 states: 6719 // 6720 // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates." 6721 // 6722 // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160 6723 // 6724 // Because of this change, and the difficulty of supporting periods with changing start 6725 // times, periods with changing start times are not supported. This makes the logic much 6726 // simpler, since periods with the same start time can be considerred the same period 6727 // across refreshes. 6728 // 6729 // To give an example as to the difficulty of handling periods where the start time may 6730 // change, if a single period manifest is refreshed with another manifest with a single 6731 // period, and both the start and end times are increased, then the only way to determine 6732 // if it's a new period or an old one that has changed is to look through the segments of 6733 // each playlist and determine the presentation time bounds to find a match. In addition, 6734 // if the period start changed to exceed the old period end, then there would be no 6735 // match, and it would not be possible to determine whether the refreshed period is a new 6736 // one or the old one. 6737 const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest)); 6738 const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that 6739 // there's a "memory leak" in that it will never stop growing, in reality, only a couple 6740 // of properties are saved for each seen Period. Even long running live streams won't 6741 // generate too many Periods, unless the stream is watched for decades. In the future, 6742 // this can be optimized by mapping to discontinuity sequence numbers for each timeline, 6743 // but it may not become an issue, and the additional info can be useful for debugging. 6744 6745 newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]); 6746 updateSequenceNumbers({ 6747 oldPlaylists, 6748 newPlaylists, 6749 timelineStarts: newManifest.timelineStarts 6750 }); 6751 return newManifest; 6752 }; 6753 6754 const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange); 6755 6756 const mergeDiscontiguousPlaylists = playlists => { 6757 // Break out playlists into groups based on their baseUrl 6758 const playlistsByBaseUrl = playlists.reduce(function (acc, cur) { 6759 if (!acc[cur.attributes.baseUrl]) { 6760 acc[cur.attributes.baseUrl] = []; 6761 } 6762 6763 acc[cur.attributes.baseUrl].push(cur); 6764 return acc; 6765 }, {}); 6766 let allPlaylists = []; 6767 Object.values(playlistsByBaseUrl).forEach(playlistGroup => { 6768 const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => { 6769 // assuming playlist IDs are the same across periods 6770 // TODO: handle multiperiod where representation sets are not the same 6771 // across periods 6772 const name = playlist.attributes.id + (playlist.attributes.lang || ''); 6773 6774 if (!acc[name]) { 6775 // First Period 6776 acc[name] = playlist; 6777 acc[name].attributes.timelineStarts = []; 6778 } else { 6779 // Subsequent Periods 6780 if (playlist.segments) { 6781 // first segment of subsequent periods signal a discontinuity 6782 if (playlist.segments[0]) { 6783 playlist.segments[0].discontinuity = true; 6784 } 6785 6786 acc[name].segments.push(...playlist.segments); 6787 } // bubble up contentProtection, this assumes all DRM content 6788 // has the same contentProtection 6789 6790 6791 if (playlist.attributes.contentProtection) { 6792 acc[name].attributes.contentProtection = playlist.attributes.contentProtection; 6793 } 6794 } 6795 6796 acc[name].attributes.timelineStarts.push({ 6797 // Although they represent the same number, it's important to have both to make it 6798 // compatible with HLS potentially having a similar attribute. 6799 start: playlist.attributes.periodStart, 6800 timeline: playlist.attributes.periodStart 6801 }); 6802 return acc; 6803 }, {})); 6804 allPlaylists = allPlaylists.concat(mergedPlaylists); 6805 }); 6806 return allPlaylists.map(playlist => { 6807 playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity'); 6808 return playlist; 6809 }); 6810 }; 6811 6812 const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => { 6813 const sidxKey = generateSidxKey(playlist.sidx); 6814 const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx; 6815 6816 if (sidxMatch) { 6817 addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri); 6818 } 6819 6820 return playlist; 6821 }; 6822 6823 const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => { 6824 if (!Object.keys(sidxMapping).length) { 6825 return playlists; 6826 } 6827 6828 for (const i in playlists) { 6829 playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping); 6830 } 6831 6832 return playlists; 6833 }; 6834 6835 const formatAudioPlaylist = ({ 6836 attributes, 6837 segments, 6838 sidx, 6839 mediaSequence, 6840 discontinuitySequence, 6841 discontinuityStarts 6842 }, isAudioOnly) => { 6843 const playlist = { 6844 attributes: { 6845 NAME: attributes.id, 6846 BANDWIDTH: attributes.bandwidth, 6847 CODECS: attributes.codecs, 6848 ['PROGRAM-ID']: 1 6849 }, 6850 uri: '', 6851 endList: attributes.type === 'static', 6852 timeline: attributes.periodStart, 6853 resolvedUri: attributes.baseUrl || '', 6854 targetDuration: attributes.duration, 6855 discontinuitySequence, 6856 discontinuityStarts, 6857 timelineStarts: attributes.timelineStarts, 6858 mediaSequence, 6859 segments 6860 }; 6861 6862 if (attributes.contentProtection) { 6863 playlist.contentProtection = attributes.contentProtection; 6864 } 6865 6866 if (attributes.serviceLocation) { 6867 playlist.attributes.serviceLocation = attributes.serviceLocation; 6868 } 6869 6870 if (sidx) { 6871 playlist.sidx = sidx; 6872 } 6873 6874 if (isAudioOnly) { 6875 playlist.attributes.AUDIO = 'audio'; 6876 playlist.attributes.SUBTITLES = 'subs'; 6877 } 6878 6879 return playlist; 6880 }; 6881 6882 const formatVttPlaylist = ({ 6883 attributes, 6884 segments, 6885 mediaSequence, 6886 discontinuityStarts, 6887 discontinuitySequence 6888 }) => { 6889 if (typeof segments === 'undefined') { 6890 // vtt tracks may use single file in BaseURL 6891 segments = [{ 6892 uri: attributes.baseUrl, 6893 timeline: attributes.periodStart, 6894 resolvedUri: attributes.baseUrl || '', 6895 duration: attributes.sourceDuration, 6896 number: 0 6897 }]; // targetDuration should be the same duration as the only segment 6898 6899 attributes.duration = attributes.sourceDuration; 6900 } 6901 6902 const m3u8Attributes = { 6903 NAME: attributes.id, 6904 BANDWIDTH: attributes.bandwidth, 6905 ['PROGRAM-ID']: 1 6906 }; 6907 6908 if (attributes.codecs) { 6909 m3u8Attributes.CODECS = attributes.codecs; 6910 } 6911 6912 const vttPlaylist = { 6913 attributes: m3u8Attributes, 6914 uri: '', 6915 endList: attributes.type === 'static', 6916 timeline: attributes.periodStart, 6917 resolvedUri: attributes.baseUrl || '', 6918 targetDuration: attributes.duration, 6919 timelineStarts: attributes.timelineStarts, 6920 discontinuityStarts, 6921 discontinuitySequence, 6922 mediaSequence, 6923 segments 6924 }; 6925 6926 if (attributes.serviceLocation) { 6927 vttPlaylist.attributes.serviceLocation = attributes.serviceLocation; 6928 } 6929 6930 return vttPlaylist; 6931 }; 6932 6933 const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => { 6934 let mainPlaylist; 6935 const formattedPlaylists = playlists.reduce((a, playlist) => { 6936 const role = playlist.attributes.role && playlist.attributes.role.value || ''; 6937 const language = playlist.attributes.lang || ''; 6938 let label = playlist.attributes.label || 'main'; 6939 6940 if (language && !playlist.attributes.label) { 6941 const roleLabel = role ? ` (${role})` : ''; 6942 label = `${playlist.attributes.lang}${roleLabel}`; 6943 } 6944 6945 if (!a[label]) { 6946 a[label] = { 6947 language, 6948 autoselect: true, 6949 default: role === 'main', 6950 playlists: [], 6951 uri: '' 6952 }; 6953 } 6954 6955 const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping); 6956 a[label].playlists.push(formatted); 6957 6958 if (typeof mainPlaylist === 'undefined' && role === 'main') { 6959 mainPlaylist = playlist; 6960 mainPlaylist.default = true; 6961 } 6962 6963 return a; 6964 }, {}); // if no playlists have role "main", mark the first as main 6965 6966 if (!mainPlaylist) { 6967 const firstLabel = Object.keys(formattedPlaylists)[0]; 6968 formattedPlaylists[firstLabel].default = true; 6969 } 6970 6971 return formattedPlaylists; 6972 }; 6973 6974 const organizeVttPlaylists = (playlists, sidxMapping = {}) => { 6975 return playlists.reduce((a, playlist) => { 6976 const label = playlist.attributes.label || playlist.attributes.lang || 'text';
6977 6978 if (!a[label]) { 6979 a[label] = { 6980 language: label
vendor: 4,243 bytes, lines 6980-7117
6980, 6981 default: false, 6982 autoselect: false, 6983 playlists: [], 6984 uri: '' 6985 }; 6986 } 6987 6988 a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping)); 6989 return a; 6990 }, {}); 6991 }; 6992 6993 const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => { 6994 if (!svc) { 6995 return svcObj; 6996 } 6997 6998 svc.forEach(service => { 6999 const { 7000 channel, 7001 language 7002 } = service; 7003 svcObj[language] = { 7004 autoselect: false, 7005 default: false, 7006 instreamId: channel, 7007 language 7008 }; 7009 7010 if (service.hasOwnProperty('aspectRatio')) { 7011 svcObj[language].aspectRatio = service.aspectRatio; 7012 } 7013 7014 if (service.hasOwnProperty('easyReader')) { 7015 svcObj[language].easyReader = service.easyReader; 7016 } 7017 7018 if (service.hasOwnProperty('3D')) { 7019 svcObj[language]['3D'] = service['3D']; 7020 } 7021 }); 7022 return svcObj; 7023 }, {}); 7024 7025 const formatVideoPlaylist = ({ 7026 attributes, 7027 segments, 7028 sidx, 7029 discontinuityStarts 7030 }) => { 7031 const playlist = { 7032 attributes: { 7033 NAME: attributes.id, 7034 AUDIO: 'audio', 7035 SUBTITLES: 'subs', 7036 RESOLUTION: { 7037 width: attributes.width, 7038 height: attributes.height 7039 }, 7040 CODECS: attributes.codecs, 7041 BANDWIDTH: attributes.bandwidth, 7042 ['PROGRAM-ID']: 1 7043 }, 7044 uri: '', 7045 endList: attributes.type === 'static', 7046 timeline: attributes.periodStart, 7047 resolvedUri: attributes.baseUrl || '', 7048 targetDuration: attributes.duration, 7049 discontinuityStarts, 7050 timelineStarts: attributes.timelineStarts, 7051 segments 7052 }; 7053 7054 if (attributes.frameRate) { 7055 playlist.attributes['FRAME-RATE'] = attributes.frameRate; 7056 } 7057 7058 if (attributes.contentProtection) { 7059 playlist.contentProtection = attributes.contentProtection; 7060 } 7061 7062 if (attributes.serviceLocation) { 7063 playlist.attributes.serviceLocation = attributes.serviceLocation; 7064 } 7065 7066 if (sidx) { 7067 playlist.sidx = sidx; 7068 } 7069 7070 return playlist; 7071 }; 7072 7073 const videoOnly = ({ 7074 attributes 7075 }) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video'; 7076 7077 const audioOnly = ({ 7078 attributes 7079 }) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio'; 7080 7081 const vttOnly = ({ 7082 attributes 7083 }) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; 7084 /** 7085 * Contains start and timeline properties denoting a timeline start. For DASH, these will 7086 * be the same number. 7087 * 7088 * @typedef {Object} TimelineStart 7089 * @property {number} start - the start time of the timeline 7090 * @property {number} timeline - the timeline number 7091 */ 7092 7093 /** 7094 * Adds appropriate media and discontinuity sequence values to the segments and playlists. 7095 * 7096 * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a 7097 * DASH specific attribute used in constructing segment URI's from templates. However, from 7098 * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence` 7099 * value, which should start at the original media sequence value (or 0) and increment by 1 7100 * for each segment thereafter. Since DASH's `startNumber` values are independent per 7101 * period, it doesn't make sense to use it for `number`. Instead, assume everything starts 7102 * from a 0 mediaSequence value and increment from there. 7103 * 7104 * Note that VHS currently doesn't use the `number` property, but it can be helpful for 7105 * debugging and making sense of the manifest. 7106 * 7107 * For live playlists, to account for values increasing in manifests when periods are 7108 * removed on refreshes, merging logic should be used to update the numbers to their 7109 * appropriate values (to ensure they're sequential and increasing). 7110 * 7111 * @param {Object[]} playlists - the playlists to update 7112 * @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest 7113 */ 7114 7115 7116 const addMediaSequenceValues = (playlists, timelineStarts) => { 7117 // increment all segments sequentially
vendor: 27,795 bytes, lines 7118-8006
7118 playlists.forEach(playlist => { 7119 playlist.mediaSequence = 0; 7120 playlist.discontinuitySequence = timelineStarts.findIndex(function ({ 7121 timeline 7122 }) { 7123 return timeline === playlist.timeline; 7124 }); 7125 7126 if (!playlist.segments) { 7127 return; 7128 } 7129 7130 playlist.segments.forEach((segment, index) => { 7131 segment.number = index; 7132 }); 7133 }); 7134 }; 7135 /** 7136 * Given a media group object, flattens all playlists within the media group into a single 7137 * array. 7138 * 7139 * @param {Object} mediaGroupObject - the media group object 7140 * 7141 * @return {Object[]} 7142 * The media group playlists 7143 */ 7144 7145 7146 const flattenMediaGroupPlaylists = mediaGroupObject => { 7147 if (!mediaGroupObject) { 7148 return []; 7149 } 7150 7151 return Object.keys(mediaGroupObject).reduce((acc, label) => { 7152 const labelContents = mediaGroupObject[label]; 7153 return acc.concat(labelContents.playlists); 7154 }, []); 7155 }; 7156 7157 const toM3u8 = ({ 7158 dashPlaylists, 7159 locations, 7160 contentSteering, 7161 sidxMapping = {}, 7162 previousManifest, 7163 eventStream 7164 }) => { 7165 if (!dashPlaylists.length) { 7166 return {}; 7167 } // grab all main manifest attributes 7168 7169 7170 const { 7171 sourceDuration: duration, 7172 type, 7173 suggestedPresentationDelay, 7174 minimumUpdatePeriod 7175 } = dashPlaylists[0].attributes; 7176 const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist); 7177 const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly)); 7178 const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly)); 7179 const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean); 7180 const manifest = { 7181 allowCache: true, 7182 discontinuityStarts: [], 7183 segments: [], 7184 endList: true, 7185 mediaGroups: { 7186 AUDIO: {}, 7187 VIDEO: {}, 7188 ['CLOSED-CAPTIONS']: {}, 7189 SUBTITLES: {} 7190 }, 7191 uri: '', 7192 duration, 7193 playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping) 7194 }; 7195 7196 if (minimumUpdatePeriod >= 0) { 7197 manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000; 7198 } 7199 7200 if (locations) { 7201 manifest.locations = locations; 7202 } 7203 7204 if (contentSteering) { 7205 manifest.contentSteering = contentSteering; 7206 } 7207 7208 if (type === 'dynamic') { 7209 manifest.suggestedPresentationDelay = suggestedPresentationDelay; 7210 } 7211 7212 if (eventStream && eventStream.length > 0) { 7213 manifest.eventStream = eventStream; 7214 } 7215 7216 const isAudioOnly = manifest.playlists.length === 0; 7217 const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null; 7218 const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null; 7219 const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup)); 7220 const playlistTimelineStarts = formattedPlaylists.map(({ 7221 timelineStarts 7222 }) => timelineStarts); 7223 manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts); 7224 addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts); 7225 7226 if (organizedAudioGroup) { 7227 manifest.mediaGroups.AUDIO.audio = organizedAudioGroup; 7228 } 7229 7230 if (organizedVttGroup) { 7231 manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup; 7232 } 7233 7234 if (captions.length) { 7235 manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions); 7236 } 7237 7238 if (previousManifest) { 7239 return positionManifestOnTimeline({ 7240 oldManifest: previousManifest, 7241 newManifest: manifest 7242 }); 7243 } 7244 7245 return manifest; 7246 }; 7247 /** 7248 * Calculates the R (repetition) value for a live stream (for the final segment 7249 * in a manifest where the r value is negative 1) 7250 * 7251 * @param {Object} attributes 7252 * Object containing all inherited attributes from parent elements with attribute 7253 * names as keys 7254 * @param {number} time 7255 * current time (typically the total time up until the final segment) 7256 * @param {number} duration 7257 * duration property for the given <S /> 7258 * 7259 * @return {number} 7260 * R value to reach the end of the given period 7261 */ 7262 7263 7264 const getLiveRValue = (attributes, time, duration) => { 7265 const { 7266 NOW, 7267 clientOffset, 7268 availabilityStartTime, 7269 timescale = 1, 7270 periodStart = 0, 7271 minimumUpdatePeriod = 0 7272 } = attributes; 7273 const now = (NOW + clientOffset) / 1000; 7274 const periodStartWC = availabilityStartTime + periodStart; 7275 const periodEndWC = now + minimumUpdatePeriod; 7276 const periodDuration = periodEndWC - periodStartWC; 7277 return Math.ceil((periodDuration * timescale - time) / duration); 7278 }; 7279 /** 7280 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment 7281 * timing and duration 7282 * 7283 * @param {Object} attributes 7284 * Object containing all inherited attributes from parent elements with attribute 7285 * names as keys 7286 * @param {Object[]} segmentTimeline 7287 * List of objects representing the attributes of each S element contained within 7288 * 7289 * @return {{number: number, duration: number, time: number, timeline: number}[]} 7290 * List of Objects with segment timing and duration info 7291 */ 7292 7293 7294 const parseByTimeline = (attributes, segmentTimeline) => { 7295 const { 7296 type, 7297 minimumUpdatePeriod = 0, 7298 media = '', 7299 sourceDuration, 7300 timescale = 1, 7301 startNumber = 1, 7302 periodStart: timeline 7303 } = attributes; 7304 const segments = []; 7305 let time = -1; 7306 7307 for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { 7308 const S = segmentTimeline[sIndex]; 7309 const duration = S.d; 7310 const repeat = S.r || 0; 7311 const segmentTime = S.t || 0; 7312 7313 if (time < 0) { 7314 // first segment 7315 time = segmentTime; 7316 } 7317 7318 if (segmentTime && segmentTime > time) { 7319 // discontinuity 7320 // TODO: How to handle this type of discontinuity 7321 // timeline++ here would treat it like HLS discontuity and content would 7322 // get appended without gap 7323 // E.G. 7324 // <S t="0" d="1" /> 7325 // <S d="1" /> 7326 // <S d="1" /> 7327 // <S t="5" d="1" /> 7328 // would have $Time$ values of [0, 1, 2, 5] 7329 // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) 7330 // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) 7331 // does the value of sourceDuration consider this when calculating arbitrary 7332 // negative @r repeat value? 7333 // E.G. Same elements as above with this added at the end 7334 // <S d="1" r="-1" /> 7335 // with a sourceDuration of 10 7336 // Would the 2 gaps be included in the time duration calculations resulting in 7337 // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments 7338 // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? 7339 time = segmentTime; 7340 } 7341 7342 let count; 7343 7344 if (repeat < 0) { 7345 const nextS = sIndex + 1; 7346 7347 if (nextS === segmentTimeline.length) { 7348 // last segment 7349 if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { 7350 count = getLiveRValue(attributes, time, duration); 7351 } else { 7352 // TODO: This may be incorrect depending on conclusion of TODO above 7353 count = (sourceDuration * timescale - time) / duration; 7354 } 7355 } else { 7356 count = (segmentTimeline[nextS].t - time) / duration; 7357 } 7358 } else { 7359 count = repeat + 1; 7360 } 7361 7362 const end = startNumber + segments.length + count; 7363 let number = startNumber + segments.length; 7364 7365 while (number < end) { 7366 segments.push({ 7367 number, 7368 duration: duration / timescale, 7369 time, 7370 timeline 7371 }); 7372 time += duration; 7373 number++; 7374 } 7375 } 7376 7377 return segments; 7378 }; 7379 7380 const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; 7381 /** 7382 * Replaces template identifiers with corresponding values. To be used as the callback 7383 * for String.prototype.replace 7384 * 7385 * @name replaceCallback 7386 * @function 7387 * @param {string} match 7388 * Entire match of identifier 7389 * @param {string} identifier 7390 * Name of matched identifier 7391 * @param {string} format 7392 * Format tag string. Its presence indicates that padding is expected 7393 * @param {string} width 7394 * Desired length of the replaced value. Values less than this width shall be left 7395 * zero padded 7396 * @return {string} 7397 * Replacement for the matched identifier 7398 */ 7399 7400 /** 7401 * Returns a function to be used as a callback for String.prototype.replace to replace 7402 * template identifiers 7403 * 7404 * @param {Obect} values 7405 * Object containing values that shall be used to replace known identifiers 7406 * @param {number} values.RepresentationID 7407 * Value of the Representation@id attribute 7408 * @param {number} values.Number 7409 * Number of the corresponding segment 7410 * @param {number} values.Bandwidth 7411 * Value of the Representation@bandwidth attribute. 7412 * @param {number} values.Time 7413 * Timestamp value of the corresponding segment 7414 * @return {replaceCallback} 7415 * Callback to be used with String.prototype.replace to replace identifiers 7416 */ 7417 7418 const identifierReplacement = values => (match, identifier, format, width) => { 7419 if (match === '$$') { 7420 // escape sequence 7421 return '$'; 7422 } 7423 7424 if (typeof values[identifier] === 'undefined') { 7425 return match; 7426 } 7427 7428 const value = '' + values[identifier]; 7429 7430 if (identifier === 'RepresentationID') { 7431 // Format tag shall not be present with RepresentationID 7432 return value; 7433 } 7434 7435 if (!format) { 7436 width = 1; 7437 } else { 7438 width = parseInt(width, 10); 7439 } 7440 7441 if (value.length >= width) { 7442 return value; 7443 } 7444 7445 return `${new Array(width - value.length + 1).join('0')}${value}`; 7446 }; 7447 /** 7448 * Constructs a segment url from a template string 7449 * 7450 * @param {string} url 7451 * Template string to construct url from 7452 * @param {Obect} values 7453 * Object containing values that shall be used to replace known identifiers 7454 * @param {number} values.RepresentationID 7455 * Value of the Representation@id attribute 7456 * @param {number} values.Number 7457 * Number of the corresponding segment 7458 * @param {number} values.Bandwidth 7459 * Value of the Representation@bandwidth attribute. 7460 * @param {number} values.Time 7461 * Timestamp value of the corresponding segment 7462 * @return {string} 7463 * Segment url with identifiers replaced 7464 */ 7465 7466 7467 const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values)); 7468 /** 7469 * Generates a list of objects containing timing and duration information about each 7470 * segment needed to generate segment uris and the complete segment object 7471 * 7472 * @param {Object} attributes 7473 * Object containing all inherited attributes from parent elements with attribute 7474 * names as keys 7475 * @param {Object[]|undefined} segmentTimeline 7476 * List of objects representing the attributes of each S element contained within 7477 * the SegmentTimeline element 7478 * @return {{number: number, duration: number, time: number, timeline: number}[]} 7479 * List of Objects with segment timing and duration info 7480 */ 7481 7482 7483 const parseTemplateInfo = (attributes, segmentTimeline) => { 7484 if (!attributes.duration && !segmentTimeline) { 7485 // if neither @duration or SegmentTimeline are present, then there shall be exactly 7486 // one media segment 7487 return [{ 7488 number: attributes.startNumber || 1, 7489 duration: attributes.sourceDuration, 7490 time: 0, 7491 timeline: attributes.periodStart 7492 }]; 7493 } 7494 7495 if (attributes.duration) { 7496 return parseByDuration(attributes); 7497 } 7498 7499 return parseByTimeline(attributes, segmentTimeline); 7500 }; 7501 /** 7502 * Generates a list of segments using information provided by the SegmentTemplate element 7503 * 7504 * @param {Object} attributes 7505 * Object containing all inherited attributes from parent elements with attribute 7506 * names as keys 7507 * @param {Object[]|undefined} segmentTimeline 7508 * List of objects representing the attributes of each S element contained within 7509 * the SegmentTimeline element 7510 * @return {Object[]} 7511 * List of segment objects 7512 */ 7513 7514 7515 const segmentsFromTemplate = (attributes, segmentTimeline) => { 7516 const templateValues = { 7517 RepresentationID: attributes.id, 7518 Bandwidth: attributes.bandwidth || 0 7519 }; 7520 const { 7521 initialization = { 7522 sourceURL: '', 7523 range: '' 7524 } 7525 } = attributes; 7526 const mapSegment = urlTypeToSegment({ 7527 baseUrl: attributes.baseUrl, 7528 source: constructTemplateUrl(initialization.sourceURL, templateValues), 7529 range: initialization.range 7530 }); 7531 const segments = parseTemplateInfo(attributes, segmentTimeline); 7532 return segments.map(segment => { 7533 templateValues.Number = segment.number; 7534 templateValues.Time = segment.time; 7535 const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2 7536 // - if timescale isn't present on any level, default to 1. 7537 7538 const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0 7539 7540 const presentationTimeOffset = attributes.presentationTimeOffset || 0; 7541 const presentationTime = // Even if the @t attribute is not specified for the segment, segment.time is 7542 // calculated in mpd-parser prior to this, so it's assumed to be available. 7543 attributes.periodStart + (segment.time - presentationTimeOffset) / timescale; 7544 const map = { 7545 uri, 7546 timeline: segment.timeline, 7547 duration: segment.duration, 7548 resolvedUri: resolveUrl$1(attributes.baseUrl || '', uri), 7549 map: mapSegment, 7550 number: segment.number, 7551 presentationTime 7552 }; 7553 return map; 7554 }); 7555 }; 7556 /** 7557 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14) 7558 * to an object that matches the output of a segment in videojs/mpd-parser 7559 * 7560 * @param {Object} attributes 7561 * Object containing all inherited attributes from parent elements with attribute 7562 * names as keys 7563 * @param {Object} segmentUrl 7564 * <SegmentURL> node to translate into a segment object 7565 * @return {Object} translated segment object 7566 */ 7567 7568 7569 const SegmentURLToSegmentObject = (attributes, segmentUrl) => { 7570 const { 7571 baseUrl, 7572 initialization = {} 7573 } = attributes; 7574 const initSegment = urlTypeToSegment({ 7575 baseUrl, 7576 source: initialization.sourceURL, 7577 range: initialization.range 7578 }); 7579 const segment = urlTypeToSegment({ 7580 baseUrl, 7581 source: segmentUrl.media, 7582 range: segmentUrl.mediaRange 7583 }); 7584 segment.map = initSegment; 7585 return segment; 7586 }; 7587 /** 7588 * Generates a list of segments using information provided by the SegmentList element 7589 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 7590 * node should be translated into segment. 7591 * 7592 * @param {Object} attributes 7593 * Object containing all inherited attributes from parent elements with attribute 7594 * names as keys 7595 * @param {Object[]|undefined} segmentTimeline 7596 * List of objects representing the attributes of each S element contained within 7597 * the SegmentTimeline element 7598 * @return {Object.<Array>} list of segments 7599 */ 7600 7601 7602 const segmentsFromList = (attributes, segmentTimeline) => { 7603 const { 7604 duration, 7605 segmentUrls = [], 7606 periodStart 7607 } = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR 7608 // if both SegmentTimeline and @duration are defined, it is outside of spec. 7609 7610 if (!duration && !segmentTimeline || duration && segmentTimeline) { 7611 throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); 7612 } 7613 7614 const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject)); 7615 let segmentTimeInfo; 7616 7617 if (duration) { 7618 segmentTimeInfo = parseByDuration(attributes); 7619 } 7620 7621 if (segmentTimeline) { 7622 segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); 7623 } 7624 7625 const segments = segmentTimeInfo.map((segmentTime, index) => { 7626 if (segmentUrlMap[index]) { 7627 const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2 7628 // - if timescale isn't present on any level, default to 1. 7629 7630 const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0 7631 7632 const presentationTimeOffset = attributes.presentationTimeOffset || 0; 7633 segment.timeline = segmentTime.timeline; 7634 segment.duration = segmentTime.duration; 7635 segment.number = segmentTime.number; 7636 segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale; 7637 return segment; 7638 } // Since we're mapping we should get rid of any blank segments (in case 7639 // the given SegmentTimeline is handling for more elements than we have 7640 // SegmentURLs for). 7641 7642 }).filter(segment => segment); 7643 return segments; 7644 }; 7645 7646 const generateSegments = ({ 7647 attributes, 7648 segmentInfo 7649 }) => { 7650 let segmentAttributes; 7651 let segmentsFn; 7652 7653 if (segmentInfo.template) { 7654 segmentsFn = segmentsFromTemplate; 7655 segmentAttributes = merge(attributes, segmentInfo.template); 7656 } else if (segmentInfo.base) { 7657 segmentsFn = segmentsFromBase; 7658 segmentAttributes = merge(attributes, segmentInfo.base); 7659 } else if (segmentInfo.list) { 7660 segmentsFn = segmentsFromList; 7661 segmentAttributes = merge(attributes, segmentInfo.list); 7662 } 7663 7664 const segmentsInfo = { 7665 attributes 7666 }; 7667 7668 if (!segmentsFn) { 7669 return segmentsInfo; 7670 } 7671 7672 const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which 7673 // must be in seconds. Since we've generated the segment list, we no longer need 7674 // @duration to be in @timescale units, so we can convert it here. 7675 7676 if (segmentAttributes.duration) { 7677 const { 7678 duration, 7679 timescale = 1 7680 } = segmentAttributes; 7681 segmentAttributes.duration = duration / timescale; 7682 } else if (segments.length) { 7683 // if there is no @duration attribute, use the largest segment duration as 7684 // as target duration 7685 segmentAttributes.duration = segments.reduce((max, segment) => { 7686 return Math.max(max, Math.ceil(segment.duration)); 7687 }, 0); 7688 } else { 7689 segmentAttributes.duration = 0; 7690 } 7691 7692 segmentsInfo.attributes = segmentAttributes; 7693 segmentsInfo.segments = segments; // This is a sidx box without actual segment information 7694 7695 if (segmentInfo.base && segmentAttributes.indexRange) { 7696 segmentsInfo.sidx = segments[0]; 7697 segmentsInfo.segments = []; 7698 } 7699 7700 return segmentsInfo; 7701 }; 7702 7703 const toPlaylists = representations => representations.map(generateSegments); 7704 7705 const findChildren = (element, name) => from(element.childNodes).filter(({ 7706 tagName 7707 }) => tagName === name); 7708 7709 const getContent = element => element.textContent.trim(); 7710 /** 7711 * Converts the provided string that may contain a division operation to a number. 7712 * 7713 * @param {string} value - the provided string value 7714 * 7715 * @return {number} the parsed string value 7716 */ 7717 7718 7719 const parseDivisionValue = value => { 7720 return parseFloat(value.split('/').reduce((prev, current) => prev / current)); 7721 }; 7722 7723 const parseDuration = str => { 7724 const SECONDS_IN_YEAR = 365 * 24 * 60 * 60; 7725 const SECONDS_IN_MONTH = 30 * 24 * 60 * 60; 7726 const SECONDS_IN_DAY = 24 * 60 * 60; 7727 const SECONDS_IN_HOUR = 60 * 60; 7728 const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S 7729 7730 const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; 7731 const match = durationRegex.exec(str); 7732 7733 if (!match) { 7734 return 0; 7735 } 7736 7737 const [year, month, day, hour, minute, second] = match.slice(1); 7738 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); 7739 }; 7740 7741 const parseDate = str => { 7742 // Date format without timezone according to ISO 8601 7743 // YYY-MM-DDThh:mm:ss.ssssss 7744 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 7745 // expressed by ending with 'Z' 7746 7747 if (dateRegex.test(str)) { 7748 str += 'Z'; 7749 } 7750 7751 return Date.parse(str); 7752 }; 7753 7754 const parsers = { 7755 /** 7756 * Specifies the duration of the entire Media Presentation. Format is a duration string 7757 * as specified in ISO 8601 7758 * 7759 * @param {string} value 7760 * value of attribute as a string 7761 * @return {number} 7762 * The duration in seconds 7763 */ 7764 mediaPresentationDuration(value) { 7765 return parseDuration(value); 7766 }, 7767 7768 /** 7769 * Specifies the Segment availability start time for all Segments referred to in this 7770 * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability 7771 * time. Format is a date string as specified in ISO 8601 7772 * 7773 * @param {string} value 7774 * value of attribute as a string 7775 * @return {number} 7776 * The date as seconds from unix epoch 7777 */ 7778 availabilityStartTime(value) { 7779 return parseDate(value) / 1000; 7780 }, 7781 7782 /** 7783 * Specifies the smallest period between potential changes to the MPD. Format is a 7784 * duration string as specified in ISO 8601 7785 * 7786 * @param {string} value 7787 * value of attribute as a string 7788 * @return {number} 7789 * The duration in seconds 7790 */ 7791 minimumUpdatePeriod(value) { 7792 return parseDuration(value); 7793 }, 7794 7795 /** 7796 * Specifies the suggested presentation delay. Format is a 7797 * duration string as specified in ISO 8601 7798 * 7799 * @param {string} value 7800 * value of attribute as a string 7801 * @return {number} 7802 * The duration in seconds 7803 */ 7804 suggestedPresentationDelay(value) { 7805 return parseDuration(value); 7806 }, 7807 7808 /** 7809 * specifices the type of mpd. Can be either "static" or "dynamic" 7810 * 7811 * @param {string} value 7812 * value of attribute as a string 7813 * 7814 * @return {string} 7815 * The type as a string 7816 */ 7817 type(value) { 7818 return value; 7819 }, 7820 7821 /** 7822 * Specifies the duration of the smallest time shifting buffer for any Representation 7823 * in the MPD. Format is a duration string as specified in ISO 8601 7824 * 7825 * @param {string} value 7826 * value of attribute as a string 7827 * @return {number} 7828 * The duration in seconds 7829 */ 7830 timeShiftBufferDepth(value) { 7831 return parseDuration(value); 7832 }, 7833 7834 /** 7835 * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. 7836 * Format is a duration string as specified in ISO 8601 7837 * 7838 * @param {string} value 7839 * value of attribute as a string 7840 * @return {number} 7841 * The duration in seconds 7842 */ 7843 start(value) { 7844 return parseDuration(value); 7845 }, 7846 7847 /** 7848 * Specifies the width of the visual presentation 7849 * 7850 * @param {string} value 7851 * value of attribute as a string 7852 * @return {number} 7853 * The parsed width 7854 */ 7855 width(value) { 7856 return parseInt(value, 10); 7857 }, 7858 7859 /** 7860 * Specifies the height of the visual presentation 7861 * 7862 * @param {string} value 7863 * value of attribute as a string 7864 * @return {number} 7865 * The parsed height 7866 */ 7867 height(value) { 7868 return parseInt(value, 10); 7869 }, 7870 7871 /** 7872 * Specifies the bitrate of the representation 7873 * 7874 * @param {string} value 7875 * value of attribute as a string 7876 * @return {number} 7877 * The parsed bandwidth 7878 */ 7879 bandwidth(value) { 7880 return parseInt(value, 10); 7881 }, 7882 7883 /** 7884 * Specifies the frame rate of the representation 7885 * 7886 * @param {string} value 7887 * value of attribute as a string 7888 * @return {number} 7889 * The parsed frame rate 7890 */ 7891 frameRate(value) { 7892 return parseDivisionValue(value); 7893 }, 7894 7895 /** 7896 * Specifies the number of the first Media Segment in this Representation in the Period 7897 * 7898 * @param {string} value 7899 * value of attribute as a string 7900 * @return {number} 7901 * The parsed number 7902 */ 7903 startNumber(value) { 7904 return parseInt(value, 10); 7905 }, 7906 7907 /** 7908 * Specifies the timescale in units per seconds 7909 * 7910 * @param {string} value 7911 * value of attribute as a string 7912 * @return {number} 7913 * The parsed timescale 7914 */ 7915 timescale(value) { 7916 return parseInt(value, 10); 7917 }, 7918 7919 /** 7920 * Specifies the presentationTimeOffset. 7921 * 7922 * @param {string} value 7923 * value of the attribute as a string 7924 * 7925 * @return {number} 7926 * The parsed presentationTimeOffset 7927 */ 7928 presentationTimeOffset(value) { 7929 return parseInt(value, 10); 7930 }, 7931 7932 /** 7933 * Specifies the constant approximate Segment duration 7934 * NOTE: The <Period> element also contains an @duration attribute. This duration 7935 * specifies the duration of the Period. This attribute is currently not 7936 * supported by the rest of the parser, however we still check for it to prevent 7937 * errors. 7938 * 7939 * @param {string} value 7940 * value of attribute as a string 7941 * @return {number} 7942 * The parsed duration 7943 */ 7944 duration(value) { 7945 const parsedValue = parseInt(value, 10); 7946 7947 if (isNaN(parsedValue)) { 7948 return parseDuration(value); 7949 } 7950 7951 return parsedValue; 7952 }, 7953 7954 /** 7955 * Specifies the Segment duration, in units of the value of the @timescale. 7956 * 7957 * @param {string} value 7958 * value of attribute as a string 7959 * @return {number} 7960 * The parsed duration 7961 */ 7962 d(value) { 7963 return parseInt(value, 10); 7964 }, 7965 7966 /** 7967 * Specifies the MPD start time, in @timescale units, the first Segment in the series 7968 * starts relative to the beginning of the Period 7969 * 7970 * @param {string} value 7971 * value of attribute as a string 7972 * @return {number} 7973 * The parsed time 7974 */ 7975 t(value) { 7976 return parseInt(value, 10); 7977 }, 7978 7979 /** 7980 * Specifies the repeat count of the number of following contiguous Segments with the 7981 * same duration expressed by the value of @d 7982 * 7983 * @param {string} value 7984 * value of attribute as a string 7985 * @return {number} 7986 * The parsed number 7987 */ 7988 r(value) { 7989 return parseInt(value, 10); 7990 }, 7991 7992 /** 7993 * Specifies the presentationTime. 7994 * 7995 * @param {string} value 7996 * value of the attribute as a string 7997 * 7998 * @return {number} 7999 * The parsed presentationTime 8000 */ 8001 presentationTime(value) { 8002 return parseInt(value, 10); 8003 }, 8004 8005 /** 8006 *
8006Default parser for all other attributes. Acts as a no-op and just returns the value 8007 * as a string 8008 * 8009 * @param {string} value 8010 * value of attribute as a string 8011 * @return {string} 8012 * Unparsed value 8013 */ 8014 DEFAULT(value) { 8015 return value; 8016 } 8017 8018 }; 8019 /** 8020 * Gets all the attributes and values of the provided node, parses attributes with known 8021 * types, and returns an object with attribute names mapped to values. 8022 * 8023 * @param {Node} el 8024 * The node to parse attributes from 8025 * @return {Object} 8026 * Object with all attributes of el parsed 8027 */ 8028 8029 const parseAttributes = el => { 8030 if (!(el && el.attributes)) { 8031 return {}; 8032 } 8033 8034 return from(el.attributes).reduce((a, e) => { 8035 const parseFn = parsers[e.name] || parsers.DEFAULT; 8036 a[e.n
vendor: 31,526 bytes, lines 8036-8894
8036ame] = parseFn(e.value); 8037 return a; 8038 }, {}); 8039 }; 8040 8041 const keySystemsMap = { 8042 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', 8043 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', 8044 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', 8045 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime', 8046 // ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme 8047 'urn:mpeg:dash:mp4protection:2011': 'mp4protection' 8048 }; 8049 /** 8050 * Builds a list of urls that is the product of the reference urls and BaseURL values 8051 * 8052 * @param {Object[]} references 8053 * List of objects containing the reference URL as well as its attributes 8054 * @param {Node[]} baseUrlElements 8055 * List of BaseURL nodes from the mpd 8056 * @return {Object[]} 8057 * List of objects with resolved urls and attributes 8058 */ 8059 8060 const buildBaseUrls = (references, baseUrlElements) => { 8061 if (!baseUrlElements.length) { 8062 return references; 8063 } 8064 8065 return flatten(references.map(function (reference) { 8066 return baseUrlElements.map(function (baseUrlElement) { 8067 const initialBaseUrl = getContent(baseUrlElement); 8068 const resolvedBaseUrl = resolveUrl$1(reference.baseUrl, initialBaseUrl); 8069 const finalBaseUrl = merge(parseAttributes(baseUrlElement), { 8070 baseUrl: resolvedBaseUrl 8071 }); // If the URL is resolved, we want to get the serviceLocation from the reference 8072 // assuming there is no serviceLocation on the initialBaseUrl 8073 8074 if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) { 8075 finalBaseUrl.serviceLocation = reference.serviceLocation; 8076 } 8077 8078 return finalBaseUrl; 8079 }); 8080 })); 8081 }; 8082 /** 8083 * Contains all Segment information for its containing AdaptationSet 8084 * 8085 * @typedef {Object} SegmentInformation 8086 * @property {Object|undefined} template 8087 * Contains the attributes for the SegmentTemplate node 8088 * @property {Object[]|undefined} segmentTimeline 8089 * Contains a list of atrributes for each S node within the SegmentTimeline node 8090 * @property {Object|undefined} list 8091 * Contains the attributes for the SegmentList node 8092 * @property {Object|undefined} base 8093 * Contains the attributes for the SegmentBase node 8094 */ 8095 8096 /** 8097 * Returns all available Segment information contained within the AdaptationSet node 8098 * 8099 * @param {Node} adaptationSet 8100 * The AdaptationSet node to get Segment information from 8101 * @return {SegmentInformation} 8102 * The Segment information contained within the provided AdaptationSet 8103 */ 8104 8105 8106 const getSegmentInformation = adaptationSet => { 8107 const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; 8108 const segmentList = findChildren(adaptationSet, 'SegmentList')[0]; 8109 const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge({ 8110 tag: 'SegmentURL' 8111 }, parseAttributes(s))); 8112 const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; 8113 const segmentTimelineParentNode = segmentList || segmentTemplate; 8114 const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; 8115 const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; 8116 const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both 8117 // @initialization and an <Initialization> node. @initialization can be templated, 8118 // while the node can have a url and range specified. If the <SegmentTemplate> has 8119 // both @initialization and an <Initialization> subelement we opt to override with 8120 // the node, as this interaction is not defined in the spec. 8121 8122 const template = segmentTemplate && parseAttributes(segmentTemplate); 8123 8124 if (template && segmentInitialization) { 8125 template.initialization = segmentInitialization && parseAttributes(segmentInitialization); 8126 } else if (template && template.initialization) { 8127 // If it is @initialization we convert it to an object since this is the format that 8128 // later functions will rely on for the initialization segment. This is only valid 8129 // for <SegmentTemplate> 8130 template.initialization = { 8131 sourceURL: template.initialization 8132 }; 8133 } 8134 8135 const segmentInfo = { 8136 template, 8137 segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)), 8138 list: segmentList && merge(parseAttributes(segmentList), { 8139 segmentUrls, 8140 initialization: parseAttributes(segmentInitialization) 8141 }), 8142 base: segmentBase && merge(parseAttributes(segmentBase), { 8143 initialization: parseAttributes(segmentInitialization) 8144 }) 8145 }; 8146 Object.keys(segmentInfo).forEach(key => { 8147 if (!segmentInfo[key]) { 8148 delete segmentInfo[key]; 8149 } 8150 }); 8151 return segmentInfo; 8152 }; 8153 /** 8154 * Contains Segment information and attributes needed to construct a Playlist object 8155 * from a Representation 8156 * 8157 * @typedef {Object} RepresentationInformation 8158 * @property {SegmentInformation} segmentInfo 8159 * Segment information for this Representation 8160 * @property {Object} attributes 8161 * Inherited attributes for this Representation 8162 */ 8163 8164 /** 8165 * Maps a Representation node to an object containing Segment information and attributes 8166 * 8167 * @name inheritBaseUrlsCallback 8168 * @function 8169 * @param {Node} representation 8170 * Representation node from the mpd 8171 * @return {RepresentationInformation} 8172 * Representation information needed to construct a Playlist object 8173 */ 8174 8175 /** 8176 * Returns a callback for Array.prototype.map for mapping Representation nodes to 8177 * Segment information and attributes using inherited BaseURL nodes. 8178 * 8179 * @param {Object} adaptationSetAttributes 8180 * Contains attributes inherited by the AdaptationSet 8181 * @param {Object[]} adaptationSetBaseUrls 8182 * List of objects containing resolved base URLs and attributes 8183 * inherited by the AdaptationSet 8184 * @param {SegmentInformation} adaptationSetSegmentInfo 8185 * Contains Segment information for the AdaptationSet 8186 * @return {inheritBaseUrlsCallback} 8187 * Callback map function 8188 */ 8189 8190 8191 const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => { 8192 const repBaseUrlElements = findChildren(representation, 'BaseURL'); 8193 const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); 8194 const attributes = merge(adaptationSetAttributes, parseAttributes(representation)); 8195 const representationSegmentInfo = getSegmentInformation(representation); 8196 return repBaseUrls.map(baseUrl => { 8197 return { 8198 segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo), 8199 attributes: merge(attributes, baseUrl) 8200 }; 8201 }); 8202 }; 8203 /** 8204 * Tranforms a series of content protection nodes to 8205 * an object containing pssh data by key system 8206 * 8207 * @param {Node[]} contentProtectionNodes 8208 * Content protection nodes 8209 * @return {Object} 8210 * Object containing pssh data by key system 8211 */ 8212 8213 8214 const generateKeySystemInformation = contentProtectionNodes => { 8215 return contentProtectionNodes.reduce((acc, node) => { 8216 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 8217 // as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system 8218 // UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do 8219 // .toLowerCase() on the input UUID string from the manifest (at least I could not think of one). 8220 8221 if (attributes.schemeIdUri) { 8222 attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase(); 8223 } 8224 8225 const keySystem = keySystemsMap[attributes.schemeIdUri]; 8226 8227 if (keySystem) { 8228 acc[keySystem] = { 8229 attributes 8230 }; 8231 const psshNode = findChildren(node, 'cenc:pssh')[0]; 8232 8233 if (psshNode) { 8234 const pssh = getContent(psshNode); 8235 acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh); 8236 } 8237 } 8238 8239 return acc; 8240 }, {}); 8241 }; // defined in ANSI_SCTE 214-1 2016 8242 8243 8244 const parseCaptionServiceMetadata = service => { 8245 // 608 captions 8246 if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') { 8247 const values = typeof service.value !== 'string' ? [] : service.value.split(';'); 8248 return values.map(value => { 8249 let channel; 8250 let language; // default language to value 8251 8252 language = value; 8253 8254 if (/^CC\d=/.test(value)) { 8255 [channel, language] = value.split('='); 8256 } else if (/^CC\d$/.test(value)) { 8257 channel = value; 8258 } 8259 8260 return { 8261 channel, 8262 language 8263 }; 8264 }); 8265 } else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') { 8266 const values = typeof service.value !== 'string' ? [] : service.value.split(';'); 8267 return values.map(value => { 8268 const flags = { 8269 // service or channel number 1-63 8270 'channel': undefined, 8271 // language is a 3ALPHA per ISO 639.2/B 8272 // field is required 8273 'language': undefined, 8274 // BIT 1/0 or ? 8275 // default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown 8276 'aspectRatio': 1, 8277 // BIT 1/0 8278 // easy reader flag indicated the text is tailed to the needs of beginning readers 8279 // default 0, or off 8280 'easyReader': 0, 8281 // BIT 1/0 8282 // If 3d metadata is present (CEA-708.1) then 1 8283 // default 0 8284 '3D': 0 8285 }; 8286 8287 if (/=/.test(value)) { 8288 const [channel, opts = ''] = value.split('='); 8289 flags.channel = channel; 8290 flags.language = value; 8291 opts.split(',').forEach(opt => { 8292 const [name, val] = opt.split(':'); 8293 8294 if (name === 'lang') { 8295 flags.language = val; // er for easyReadery 8296 } else if (name === 'er') { 8297 flags.easyReader = Number(val); // war for wide aspect ratio 8298 } else if (name === 'war') { 8299 flags.aspectRatio = Number(val); 8300 } else if (name === '3D') { 8301 flags['3D'] = Number(val); 8302 } 8303 }); 8304 } else { 8305 flags.language = value; 8306 } 8307 8308 if (flags.channel) { 8309 flags.channel = 'SERVICE' + flags.channel; 8310 } 8311 8312 return flags; 8313 }); 8314 } 8315 }; 8316 /** 8317 * A map callback that will parse all event stream data for a collection of periods 8318 * DASH ISO_IEC_23009 5.10.2.2 8319 * https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing 8320 * 8321 * @param {PeriodInformation} period object containing necessary period information 8322 * @return a collection of parsed eventstream event objects 8323 */ 8324 8325 8326 const toEventStream = period => { 8327 // get and flatten all EventStreams tags and parse attributes and children 8328 return flatten(findChildren(period.node, 'EventStream').map(eventStream => { 8329 const eventStreamAttributes = parseAttributes(eventStream); 8330 const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects 8331 8332 return findChildren(eventStream, 'Event').map(event => { 8333 const eventAttributes = parseAttributes(event); 8334 const presentationTime = eventAttributes.presentationTime || 0; 8335 const timescale = eventStreamAttributes.timescale || 1; 8336 const duration = eventAttributes.duration || 0; 8337 const start = presentationTime / timescale + period.attributes.start; 8338 return { 8339 schemeIdUri, 8340 value: eventStreamAttributes.value, 8341 id: eventAttributes.id, 8342 start, 8343 end: start + duration / timescale, 8344 messageData: getContent(event) || eventAttributes.messageData, 8345 contentEncoding: eventStreamAttributes.contentEncoding, 8346 presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0 8347 }; 8348 }); 8349 })); 8350 }; 8351 /** 8352 * Maps an AdaptationSet node to a list of Representation information objects 8353 * 8354 * @name toRepresentationsCallback 8355 * @function 8356 * @param {Node} adaptationSet 8357 * AdaptationSet node from the mpd 8358 * @return {RepresentationInformation[]} 8359 * List of objects containing Representaion information 8360 */ 8361 8362 /** 8363 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of 8364 * Representation information objects 8365 * 8366 * @param {Object} periodAttributes 8367 * Contains attributes inherited by the Period 8368 * @param {Object[]} periodBaseUrls 8369 * Contains list of objects with resolved base urls and attributes 8370 * inherited by the Period 8371 * @param {string[]} periodSegmentInfo 8372 * Contains Segment Information at the period level 8373 * @return {toRepresentationsCallback} 8374 * Callback map function 8375 */ 8376 8377 8378 const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => { 8379 const adaptationSetAttributes = parseAttributes(adaptationSet); 8380 const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); 8381 const role = findChildren(adaptationSet, 'Role')[0]; 8382 const roleAttributes = { 8383 role: parseAttributes(role) 8384 }; 8385 let attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes); 8386 const accessibility = findChildren(adaptationSet, 'Accessibility')[0]; 8387 const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility)); 8388 8389 if (captionServices) { 8390 attrs = merge(attrs, { 8391 captionServices 8392 }); 8393 } 8394 8395 const label = findChildren(adaptationSet, 'Label')[0]; 8396 8397 if (label && label.childNodes.length) { 8398 const labelVal = label.childNodes[0].nodeValue.trim(); 8399 attrs = merge(attrs, { 8400 label: labelVal 8401 }); 8402 } 8403 8404 const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); 8405 8406 if (Object.keys(contentProtection).length) { 8407 attrs = merge(attrs, { 8408 contentProtection 8409 }); 8410 } 8411 8412 const segmentInfo = getSegmentInformation(adaptationSet); 8413 const representations = findChildren(adaptationSet, 'Representation'); 8414 const adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo); 8415 return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); 8416 }; 8417 /** 8418 * Contains all period information for mapping nodes onto adaptation sets. 8419 * 8420 * @typedef {Object} PeriodInformation 8421 * @property {Node} period.node 8422 * Period node from the mpd 8423 * @property {Object} period.attributes 8424 * Parsed period attributes from node plus any added 8425 */ 8426 8427 /** 8428 * Maps a PeriodInformation object to a list of Representation information objects for all 8429 * AdaptationSet nodes contained within the Period. 8430 * 8431 * @name toAdaptationSetsCallback 8432 * @function 8433 * @param {PeriodInformation} period 8434 * Period object containing necessary period information 8435 * @param {number} periodStart 8436 * Start time of the Period within the mpd 8437 * @return {RepresentationInformation[]} 8438 * List of objects containing Representaion information 8439 */ 8440 8441 /** 8442 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of 8443 * Representation information objects 8444 * 8445 * @param {Object} mpdAttributes 8446 * Contains attributes inherited by the mpd 8447 * @param {Object[]} mpdBaseUrls 8448 * Contains list of objects with resolved base urls and attributes 8449 * inherited by the mpd 8450 * @return {toAdaptationSetsCallback} 8451 * Callback map function 8452 */ 8453 8454 8455 const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => { 8456 const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL')); 8457 const periodAttributes = merge(mpdAttributes, { 8458 periodStart: period.attributes.start 8459 }); 8460 8461 if (typeof period.attributes.duration === 'number') { 8462 periodAttributes.periodDuration = period.attributes.duration; 8463 } 8464 8465 const adaptationSets = findChildren(period.node, 'AdaptationSet'); 8466 const periodSegmentInfo = getSegmentInformation(period.node); 8467 return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); 8468 }; 8469 /** 8470 * Tranforms an array of content steering nodes into an object 8471 * containing CDN content steering information from the MPD manifest. 8472 * 8473 * For more information on the DASH spec for Content Steering parsing, see: 8474 * https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 8475 * 8476 * @param {Node[]} contentSteeringNodes 8477 * Content steering nodes 8478 * @param {Function} eventHandler 8479 * The event handler passed into the parser options to handle warnings 8480 * @return {Object} 8481 * Object containing content steering data 8482 */ 8483 8484 8485 const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => { 8486 // If there are more than one ContentSteering tags, throw an error 8487 if (contentSteeringNodes.length > 1) { 8488 eventHandler({ 8489 type: 'warn', 8490 message: 'The MPD manifest should contain no more than one ContentSteering tag' 8491 }); 8492 } // Return a null value if there are no ContentSteering tags 8493 8494 8495 if (!contentSteeringNodes.length) { 8496 return null; 8497 } 8498 8499 const infoFromContentSteeringTag = merge({ 8500 serverURL: getContent(contentSteeringNodes[0]) 8501 }, parseAttributes(contentSteeringNodes[0])); // Converts `queryBeforeStart` to a boolean, as well as setting the default value 8502 // to `false` if it doesn't exist 8503 8504 infoFromContentSteeringTag.queryBeforeStart = infoFromContentSteeringTag.queryBeforeStart === 'true'; 8505 return infoFromContentSteeringTag; 8506 }; 8507 /** 8508 * Gets Period@start property for a given period. 8509 * 8510 * @param {Object} options 8511 * Options object 8512 * @param {Object} options.attributes 8513 * Period attributes 8514 * @param {Object} [options.priorPeriodAttributes] 8515 * Prior period attributes (if prior period is available) 8516 * @param {string} options.mpdType 8517 * The MPD@type these periods came from 8518 * @return {number|null} 8519 * The period start, or null if it's an early available period or error 8520 */ 8521 8522 8523 const getPeriodStart = ({ 8524 attributes, 8525 priorPeriodAttributes, 8526 mpdType 8527 }) => { 8528 // Summary of period start time calculation from DASH spec section 5.3.2.1 8529 // 8530 // A period's start is the first period's start + time elapsed after playing all 8531 // prior periods to this one. Periods continue one after the other in time (without 8532 // gaps) until the end of the presentation. 8533 // 8534 // The value of Period@start should be: 8535 // 1. if Period@start is present: value of Period@start 8536 // 2. if previous period exists and it has @duration: previous Period@start + 8537 // previous Period@duration 8538 // 3. if this is first period and MPD@type is 'static': 0 8539 // 4. in all other cases, consider the period an "early available period" (note: not 8540 // currently supported) 8541 // (1) 8542 if (typeof attributes.start === 'number') { 8543 return attributes.start; 8544 } // (2) 8545 8546 8547 if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') { 8548 return priorPeriodAttributes.start + priorPeriodAttributes.duration; 8549 } // (3) 8550 8551 8552 if (!priorPeriodAttributes && mpdType === 'static') { 8553 return 0; 8554 } // (4) 8555 // There is currently no logic for calculating the Period@start value if there is 8556 // no Period@start or prior Period@start and Period@duration available. This is not made 8557 // explicit by the DASH interop guidelines or the DASH spec, however, since there's 8558 // nothing about any other resolution strategies, it's implied. Thus, this case should 8559 // be considered an early available period, or error, and null should suffice for both 8560 // of those cases. 8561 8562 8563 return null; 8564 }; 8565 /** 8566 * Traverses the mpd xml tree to generate a list of Representation information objects 8567 * that have inherited attributes from parent nodes 8568 * 8569 * @param {Node} mpd 8570 * The root node of the mpd 8571 * @param {Object} options 8572 * Available options for inheritAttributes 8573 * @param {string} options.manifestUri 8574 * The uri source of the mpd 8575 * @param {number} options.NOW 8576 * Current time per DASH IOP. Default is current time in ms since epoch 8577 * @param {number} options.clientOffset 8578 * Client time difference from NOW (in milliseconds) 8579 * @return {RepresentationInformation[]} 8580 * List of objects containing Representation information 8581 */ 8582 8583 8584 const inheritAttributes = (mpd, options = {}) => { 8585 const { 8586 manifestUri = '', 8587 NOW = Date.now(), 8588 clientOffset = 0, 8589 // TODO: For now, we are expecting an eventHandler callback function 8590 // to be passed into the mpd parser as an option. 8591 // In the future, we should enable stream parsing by using the Stream class from vhs-utils. 8592 // This will support new features including a standardized event handler. 8593 // See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing. 8594 // https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9 8595 eventHandler = function () {} 8596 } = options; 8597 const periodNodes = findChildren(mpd, 'Period'); 8598 8599 if (!periodNodes.length) { 8600 throw new Error(errors.INVALID_NUMBER_OF_PERIOD); 8601 } 8602 8603 const locations = findChildren(mpd, 'Location'); 8604 const mpdAttributes = parseAttributes(mpd); 8605 const mpdBaseUrls = buildBaseUrls([{ 8606 baseUrl: manifestUri 8607 }], findChildren(mpd, 'BaseURL')); 8608 const contentSteeringNodes = findChildren(mpd, 'ContentSteering'); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'. 8609 8610 mpdAttributes.type = mpdAttributes.type || 'static'; 8611 mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; 8612 mpdAttributes.NOW = NOW; 8613 mpdAttributes.clientOffset = clientOffset; 8614 8615 if (locations.length) { 8616 mpdAttributes.locations = locations.map(getContent); 8617 } 8618 8619 const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to 8620 // adding properties that require looking at prior periods is to parse attributes and add 8621 // missing ones before toAdaptationSets is called. If more such properties are added, it 8622 // may be better to refactor toAdaptationSets. 8623 8624 periodNodes.forEach((node, index) => { 8625 const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary 8626 // for this period. 8627 8628 const priorPeriod = periods[index - 1]; 8629 attributes.start = getPeriodStart({ 8630 attributes, 8631 priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null, 8632 mpdType: mpdAttributes.type 8633 }); 8634 periods.push({ 8635 node, 8636 attributes 8637 }); 8638 }); 8639 return { 8640 locations: mpdAttributes.locations, 8641 contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler), 8642 // TODO: There are occurences where this `representationInfo` array contains undesired 8643 // duplicates. This generally occurs when there are multiple BaseURL nodes that are 8644 // direct children of the MPD node. When we attempt to resolve URLs from a combination of the 8645 // parent BaseURL and a child BaseURL, and the value does not resolve, 8646 // we end up returning the child BaseURL multiple times. 8647 // We need to determine a way to remove these duplicates in a safe way. 8648 // See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527 8649 representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))), 8650 eventStream: flatten(periods.map(toEventStream)) 8651 }; 8652 }; 8653 8654 const stringToMpdXml = manifestString => { 8655 if (manifestString === '') { 8656 throw new Error(errors.DASH_EMPTY_MANIFEST); 8657 } 8658 8659 const parser = new xmldom.DOMParser(); 8660 let xml; 8661 let mpd; 8662 8663 try { 8664 xml = parser.parseFromString(manifestString, 'application/xml'); 8665 mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; 8666 } catch (e) {// ie 11 throws on invalid xml 8667 } 8668 8669 if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { 8670 throw new Error(errors.DASH_INVALID_XML); 8671 } 8672 8673 return mpd; 8674 }; 8675 /** 8676 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 8677 * 8678 * @param {string} mpd 8679 * XML string of the MPD manifest 8680 * @return {Object|null} 8681 * Attributes of UTCTiming node specified in the manifest. Null if none found 8682 */ 8683 8684 8685 const parseUTCTimingScheme = mpd => { 8686 const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; 8687 8688 if (!UTCTimingNode) { 8689 return null; 8690 } 8691 8692 const attributes = parseAttributes(UTCTimingNode); 8693 8694 switch (attributes.schemeIdUri) { 8695 case 'urn:mpeg:dash:utc:http-head:2014': 8696 case 'urn:mpeg:dash:utc:http-head:2012': 8697 attributes.method = 'HEAD'; 8698 break; 8699 8700 case 'urn:mpeg:dash:utc:http-xsdate:2014': 8701 case 'urn:mpeg:dash:utc:http-iso:2014': 8702 case 'urn:mpeg:dash:utc:http-xsdate:2012': 8703 case 'urn:mpeg:dash:utc:http-iso:2012': 8704 attributes.method = 'GET'; 8705 break; 8706 8707 case 'urn:mpeg:dash:utc:direct:2014': 8708 case 'urn:mpeg:dash:utc:direct:2012': 8709 attributes.method = 'DIRECT'; 8710 attributes.value = Date.parse(attributes.value); 8711 break; 8712 8713 case 'urn:mpeg:dash:utc:http-ntp:2014': 8714 case 'urn:mpeg:dash:utc:ntp:2014': 8715 case 'urn:mpeg:dash:utc:sntp:2014': 8716 default: 8717 throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); 8718 } 8719 8720 return attributes; 8721 }; 8722 /* 8723 * Given a DASH manifest string and options, parses the DASH manifest into an object in the 8724 * form outputed by m3u8-parser and accepted by videojs/http-streaming. 8725 * 8726 * For live DASH manifests, if `previousManifest` is provided in options, then the newly 8727 * parsed DASH manifest will have its media sequence and discontinuity sequence values 8728 * updated to reflect its position relative to the prior manifest. 8729 * 8730 * @param {string} manifestString - the DASH manifest as a string 8731 * @param {options} [options] - any options 8732 * 8733 * @return {Object} the manifest object 8734 */ 8735 8736 const parse = (manifestString, options = {}) => { 8737 const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options); 8738 const playlists = toPlaylists(parsedManifestInfo.representationInfo); 8739 return toM3u8({ 8740 dashPlaylists: playlists, 8741 locations: parsedManifestInfo.locations, 8742 contentSteering: parsedManifestInfo.contentSteeringInfo, 8743 sidxMapping: options.sidxMapping, 8744 previousManifest: options.previousManifest, 8745 eventStream: parsedManifestInfo.eventStream 8746 }); 8747 }; 8748 /** 8749 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 8750 * 8751 * @param {string} manifestString 8752 * XML string of the MPD manifest 8753 * @return {Object|null} 8754 * Attributes of UTCTiming node specified in the manifest. Null if none found 8755 */ 8756 8757 8758 const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString)); 8759 8760 var MAX_UINT32 = Math.pow(2, 32); 8761 8762 var getUint64$1 = function (uint8) { 8763 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 8764 var value; 8765 8766 if (dv.getBigUint64) { 8767 value = dv.getBigUint64(0); 8768 8769 if (value < Number.MAX_SAFE_INTEGER) { 8770 return Number(value); 8771 } 8772 8773 return value; 8774 } 8775 8776 return dv.getUint32(0) * MAX_UINT32 + dv.getUint32(4); 8777 }; 8778 8779 var numbers = { 8780 getUint64: getUint64$1, 8781 MAX_UINT32: MAX_UINT32 8782 }; 8783 8784 var getUint64 = numbers.getUint64; 8785 8786 var parseSidx = function (data) { 8787 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 8788 result = { 8789 version: data[0], 8790 flags: new Uint8Array(data.subarray(1, 4)), 8791 references: [], 8792 referenceId: view.getUint32(4), 8793 timescale: view.getUint32(8) 8794 }, 8795 i = 12; 8796 8797 if (result.version === 0) { 8798 result.earliestPresentationTime = view.getUint32(i); 8799 result.firstOffset = view.getUint32(i + 4); 8800 i += 8; 8801 } else { 8802 // read 64 bits 8803 result.earliestPresentationTime = getUint64(data.subarray(i)); 8804 result.firstOffset = getUint64(data.subarray(i + 8)); 8805 i += 16; 8806 } 8807 8808 i += 2; // reserved 8809 8810 var referenceCount = view.getUint16(i); 8811 i += 2; // start of references 8812 8813 for (; referenceCount > 0; i += 12, referenceCount--) { 8814 result.references.push({ 8815 referenceType: (data[i] & 0x80) >>> 7, 8816 referencedSize: view.getUint32(i) & 0x7FFFFFFF, 8817 subsegmentDuration: view.getUint32(i + 4), 8818 startsWithSap: !!(data[i + 8] & 0x80), 8819 sapType: (data[i + 8] & 0x70) >>> 4, 8820 sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF 8821 }); 8822 } 8823 8824 return result; 8825 }; 8826 8827 var parseSidx_1 = parseSidx; 8828 8829 var ID3 = toUint8([0x49, 0x44, 0x33]); 8830 var getId3Size = function getId3Size(bytes, offset) { 8831 if (offset === void 0) { 8832 offset = 0; 8833 } 8834 8835 bytes = toUint8(bytes); 8836 var flags = bytes[offset + 5]; 8837 var returnSize = bytes[offset + 6] << 21 | bytes[offset + 7] << 14 | bytes[offset + 8] << 7 | bytes[offset + 9]; 8838 var footerPresent = (flags & 16) >> 4; 8839 8840 if (footerPresent) { 8841 return returnSize + 20; 8842 } 8843 8844 return returnSize + 10; 8845 }; 8846 var getId3Offset = function getId3Offset(bytes, offset) { 8847 if (offset === void 0) { 8848 offset = 0; 8849 } 8850 8851 bytes = toUint8(bytes); 8852 8853 if (bytes.length - offset < 10 || !bytesMatch(bytes, ID3, { 8854 offset: offset 8855 })) { 8856 return offset; 8857 } 8858 8859 offset += getId3Size(bytes, offset); // recursive check for id3 tags as some files 8860 // have multiple ID3 tag sections even though 8861 // they should not. 8862 8863 return getId3Offset(bytes, offset); 8864 }; 8865 8866 var normalizePath$1 = function normalizePath(path) { 8867 if (typeof path === 'string') { 8868 return stringToBytes(path); 8869 } 8870 8871 if (typeof path === 'number') { 8872 return path; 8873 } 8874 8875 return path; 8876 }; 8877 8878 var normalizePaths$1 = function normalizePaths(paths) { 8879 if (!Array.isArray(paths)) { 8880 return [normalizePath$1(paths)]; 8881 } 8882 8883 return paths.map(function (p) { 8884 return normalizePath$1(p); 8885 }); 8886 }; 8887 /** 8888 * find any number of boxes by name given a path to it in an iso bmff 8889 * such as mp4. 8890 * 8891 * @param {TypedArray} bytes 8892 * bytes for the iso bmff to search for boxes in 8893 * 8894 * @param {Uint8Array[]|string[]|string|Uint8Array}
vendor: 9,470 bytes, lines 8894-9209
8894 name 8895 * An array of paths or a single path representing the name 8896 * of boxes to search through in bytes. Paths may be 8897 * uint8 (character codes) or strings. 8898 * 8899 * @param {boolean} [complete=false] 8900 * Should we search only for complete boxes on the final path. 8901 * This is very useful when you do not want to get back partial boxes 8902 * in the case of streaming files. 8903 * 8904 * @return {Uint8Array[]} 8905 * An array of the end paths that we found. 8906 */ 8907 8908 var findBox = function findBox(bytes, paths, complete) { 8909 if (complete === void 0) { 8910 complete = false; 8911 } 8912 8913 paths = normalizePaths$1(paths); 8914 bytes = toUint8(bytes); 8915 var results = []; 8916 8917 if (!paths.length) { 8918 // short-circuit the search for empty paths 8919 return results; 8920 } 8921 8922 var i = 0; 8923 8924 while (i < bytes.length) { 8925 var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0; 8926 var type = bytes.subarray(i + 4, i + 8); // invalid box format. 8927 8928 if (size === 0) { 8929 break; 8930 } 8931 8932 var end = i + size; 8933 8934 if (end > bytes.length) { 8935 // this box is bigger than the number of bytes we have 8936 // and complete is set, we cannot find any more boxes. 8937 if (complete) { 8938 break; 8939 } 8940 8941 end = bytes.length; 8942 } 8943 8944 var data = bytes.subarray(i + 8, end); 8945 8946 if (bytesMatch(type, paths[0])) { 8947 if (paths.length === 1) { 8948 // this is the end of the path and we've found the box we were 8949 // looking for 8950 results.push(data); 8951 } else { 8952 // recursively search for the next box along the path 8953 results.push.apply(results, findBox(data, paths.slice(1), complete)); 8954 } 8955 } 8956 8957 i = end; 8958 } // we've finished searching all of bytes 8959 8960 8961 return results; 8962 }; 8963 8964 // https://matroska-org.github.io/libebml/specs.html 8965 // https://www.matroska.org/technical/elements.html 8966 // https://www.webmproject.org/docs/container/ 8967 8968 var EBML_TAGS = { 8969 EBML: toUint8([0x1A, 0x45, 0xDF, 0xA3]), 8970 DocType: toUint8([0x42, 0x82]), 8971 Segment: toUint8([0x18, 0x53, 0x80, 0x67]), 8972 SegmentInfo: toUint8([0x15, 0x49, 0xA9, 0x66]), 8973 Tracks: toUint8([0x16, 0x54, 0xAE, 0x6B]), 8974 Track: toUint8([0xAE]), 8975 TrackNumber: toUint8([0xd7]), 8976 DefaultDuration: toUint8([0x23, 0xe3, 0x83]), 8977 TrackEntry: toUint8([0xAE]), 8978 TrackType: toUint8([0x83]), 8979 FlagDefault: toUint8([0x88]), 8980 CodecID: toUint8([0x86]), 8981 CodecPrivate: toUint8([0x63, 0xA2]), 8982 VideoTrack: toUint8([0xe0]), 8983 AudioTrack: toUint8([0xe1]), 8984 // Not used yet, but will be used for live webm/mkv 8985 // see https://www.matroska.org/technical/basics.html#block-structure 8986 // see https://www.matroska.org/technical/basics.html#simpleblock-structure 8987 Cluster: toUint8([0x1F, 0x43, 0xB6, 0x75]), 8988 Timestamp: toUint8([0xE7]), 8989 TimestampScale: toUint8([0x2A, 0xD7, 0xB1]), 8990 BlockGroup: toUint8([0xA0]), 8991 BlockDuration: toUint8([0x9B]), 8992 Block: toUint8([0xA1]), 8993 SimpleBlock: toUint8([0xA3]) 8994 }; 8995 /** 8996 * This is a simple table to determine the length 8997 * of things in ebml. The length is one based (starts at 1, 8998 * rather than zero) and for every zero bit before a one bit 8999 * we add one to length. We also need this table because in some 9000 * case we have to xor all the length bits from another value. 9001 */ 9002 9003 var LENGTH_TABLE = [128, 64, 32, 16, 8, 4, 2, 1]; 9004 9005 var getLength = function getLength(byte) { 9006 var len = 1; 9007 9008 for (var i = 0; i < LENGTH_TABLE.length; i++) { 9009 if (byte & LENGTH_TABLE[i]) { 9010 break; 9011 } 9012 9013 len++; 9014 } 9015 9016 return len; 9017 }; // length in ebml is stored in the first 4 to 8 bits 9018 // of the first byte. 4 for the id length and 8 for the 9019 // data size length. Length is measured by converting the number to binary 9020 // then 1 + the number of zeros before a 1 is encountered starting 9021 // from the left. 9022 9023 9024 var getvint = function getvint(bytes, offset, removeLength, signed) { 9025 if (removeLength === void 0) { 9026 removeLength = true; 9027 } 9028 9029 if (signed === void 0) { 9030 signed = false; 9031 } 9032 9033 var length = getLength(bytes[offset]); 9034 var valueBytes = bytes.subarray(offset, offset + length); // NOTE that we do **not** subarray here because we need to copy these bytes 9035 // as they will be modified below to remove the dataSizeLen bits and we do not 9036 // want to modify the original data. normally we could just call slice on 9037 // uint8array but ie 11 does not support that... 9038 9039 if (removeLength) { 9040 valueBytes = Array.prototype.slice.call(bytes, offset, offset + length); 9041 valueBytes[0] ^= LENGTH_TABLE[length - 1]; 9042 } 9043 9044 return { 9045 length: length, 9046 value: bytesToNumber(valueBytes, { 9047 signed: signed 9048 }), 9049 bytes: valueBytes 9050 }; 9051 }; 9052 9053 var normalizePath = function normalizePath(path) { 9054 if (typeof path === 'string') { 9055 return path.match(/.{1,2}/g).map(function (p) { 9056 return normalizePath(p); 9057 }); 9058 } 9059 9060 if (typeof path === 'number') { 9061 return numberToBytes(path); 9062 } 9063 9064 return path; 9065 }; 9066 9067 var normalizePaths = function normalizePaths(paths) { 9068 if (!Array.isArray(paths)) { 9069 return [normalizePath(paths)]; 9070 } 9071 9072 return paths.map(function (p) { 9073 return normalizePath(p); 9074 }); 9075 }; 9076 9077 var getInfinityDataSize = function getInfinityDataSize(id, bytes, offset) { 9078 if (offset >= bytes.length) { 9079 return bytes.length; 9080 } 9081 9082 var innerid = getvint(bytes, offset, false); 9083 9084 if (bytesMatch(id.bytes, innerid.bytes)) { 9085 return offset; 9086 } 9087 9088 var dataHeader = getvint(bytes, offset + innerid.length); 9089 return getInfinityDataSize(id, bytes, offset + dataHeader.length + dataHeader.value + innerid.length); 9090 }; 9091 /** 9092 * Notes on the EBLM format. 9093 * 9094 * EBLM uses "vints" tags. Every vint tag contains 9095 * two parts 9096 * 9097 * 1. The length from the first byte. You get this by 9098 * converting the byte to binary and counting the zeros 9099 * before a 1. Then you add 1 to that. Examples 9100 * 00011111 = length 4 because there are 3 zeros before a 1. 9101 * 00100000 = length 3 because there are 2 zeros before a 1. 9102 * 00000011 = length 7 because there are 6 zeros before a 1. 9103 * 9104 * 2. The bits used for length are removed from the first byte 9105 * Then all the bytes are merged into a value. NOTE: this 9106 * is not the case for id ebml tags as there id includes 9107 * length bits. 9108 * 9109 */ 9110 9111 9112 var findEbml = function findEbml(bytes, paths) { 9113 paths = normalizePaths(paths); 9114 bytes = toUint8(bytes); 9115 var results = []; 9116 9117 if (!paths.length) { 9118 return results; 9119 } 9120 9121 var i = 0; 9122 9123 while (i < bytes.length) { 9124 var id = getvint(bytes, i, false); 9125 var dataHeader = getvint(bytes, i + id.length); 9126 var dataStart = i + id.length + dataHeader.length; // dataSize is unknown or this is a live stream 9127 9128 if (dataHeader.value === 0x7f) { 9129 dataHeader.value = getInfinityDataSize(id, bytes, dataStart); 9130 9131 if (dataHeader.value !== bytes.length) { 9132 dataHeader.value -= dataStart; 9133 } 9134 } 9135 9136 var dataEnd = dataStart + dataHeader.value > bytes.length ? bytes.length : dataStart + dataHeader.value; 9137 var data = bytes.subarray(dataStart, dataEnd); 9138 9139 if (bytesMatch(paths[0], id.bytes)) { 9140 if (paths.length === 1) { 9141 // this is the end of the paths and we've found the tag we were 9142 // looking for 9143 results.push(data); 9144 } else { 9145 // recursively search for the next tag inside of the data 9146 // of this one 9147 results = results.concat(findEbml(data, paths.slice(1))); 9148 } 9149 } 9150 9151 var totalLength = id.length + dataHeader.length + data.length; // move past this tag entirely, we are not looking for it 9152 9153 i += totalLength; 9154 } 9155 9156 return results; 9157 }; // see https://www.matroska.org/technical/basics.html#block-structure 9158 9159 var NAL_TYPE_ONE = toUint8([0x00, 0x00, 0x00, 0x01]); 9160 var NAL_TYPE_TWO = toUint8([0x00, 0x00, 0x01]); 9161 var EMULATION_PREVENTION = toUint8([0x00, 0x00, 0x03]); 9162 /** 9163 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 9164 * Sequence Payload" 9165 * 9166 * @param data {Uint8Array} the bytes of a RBSP from a NAL 9167 * unit 9168 * @return {Uint8Array} the RBSP without any Emulation 9169 * Prevention Bytes 9170 */ 9171 9172 var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(bytes) { 9173 var positions = []; 9174 var i = 1; // Find all `Emulation Prevention Bytes` 9175 9176 while (i < bytes.length - 2) { 9177 if (bytesMatch(bytes.subarray(i, i + 3), EMULATION_PREVENTION)) { 9178 positions.push(i + 2); 9179 i++; 9180 } 9181 9182 i++; 9183 } // If no Emulation Prevention Bytes were found just return the original 9184 // array 9185 9186 9187 if (positions.length === 0) { 9188 return bytes; 9189 } // Create a new array to hold the NAL unit data 9190 9191 9192 var newLength = bytes.length - positions.length; 9193 var newData = new Uint8Array(newLength); 9194 var sourceIndex = 0; 9195 9196 for (i = 0; i < newLength; sourceIndex++, i++) { 9197 if (sourceIndex === positions[0]) { 9198 // Skip this byte 9199 sourceIndex++; // Remove this position index 9200 9201 positions.shift(); 9202 } 9203 9204 newData[i] = bytes[sourceIndex]; 9205 } 9206 9207 return newData; 9208 }; 9209 var findNal = function findNal(bytes, dataType, types, nalLimit) {
9210 if (nalLimit === void 0) { 9211 nalLimit = Infinity; 9212 }
vendor: 7,157 bytes, lines 9212-9450
9212 9213 9214 bytes = toUint8(bytes); 9215 types = [].concat(types); 9216 var i = 0; 9217 var nalStart; 9218 var nalsFound = 0; // keep searching until: 9219 // we reach the end of bytes 9220 // we reach the maximum number of nals they want to seach 9221 // NOTE: that we disregard nalLimit when we have found the start 9222 // of the nal we want so that we can find the end of the nal we want. 9223 9224 while (i < bytes.length && (nalsFound < nalLimit || nalStart)) { 9225 var nalOffset = void 0; 9226 9227 if (bytesMatch(bytes.subarray(i), NAL_TYPE_ONE)) { 9228 nalOffset = 4; 9229 } else if (bytesMatch(bytes.subarray(i), NAL_TYPE_TWO)) { 9230 nalOffset = 3; 9231 } // we are unsynced, 9232 // find the next nal unit 9233 9234 9235 if (!nalOffset) { 9236 i++; 9237 continue; 9238 } 9239 9240 nalsFound++; 9241 9242 if (nalStart) { 9243 return discardEmulationPreventionBytes(bytes.subarray(nalStart, i)); 9244 } 9245 9246 var nalType = void 0; 9247 9248 if (dataType === 'h264') { 9249 nalType = bytes[i + nalOffset] & 0x1f; 9250 } else if (dataType === 'h265') { 9251 nalType = bytes[i + nalOffset] >> 1 & 0x3f; 9252 } 9253 9254 if (types.indexOf(nalType) !== -1) { 9255 nalStart = i + nalOffset; 9256 } // nal header is 1 length for h264, and 2 for h265 9257 9258 9259 i += nalOffset + (dataType === 'h264' ? 1 : 2); 9260 } 9261 9262 return bytes.subarray(0, 0); 9263 }; 9264 var findH264Nal = function findH264Nal(bytes, type, nalLimit) { 9265 return findNal(bytes, 'h264', type, nalLimit); 9266 }; 9267 var findH265Nal = function findH265Nal(bytes, type, nalLimit) { 9268 return findNal(bytes, 'h265', type, nalLimit); 9269 }; 9270 9271 var CONSTANTS = { 9272 // "webm" string literal in hex 9273 'webm': toUint8([0x77, 0x65, 0x62, 0x6d]), 9274 // "matroska" string literal in hex 9275 'matroska': toUint8([0x6d, 0x61, 0x74, 0x72, 0x6f, 0x73, 0x6b, 0x61]), 9276 // "fLaC" string literal in hex 9277 'flac': toUint8([0x66, 0x4c, 0x61, 0x43]), 9278 // "OggS" string literal in hex 9279 'ogg': toUint8([0x4f, 0x67, 0x67, 0x53]), 9280 // ac-3 sync byte, also works for ec-3 as that is simply a codec 9281 // of ac-3 9282 'ac3': toUint8([0x0b, 0x77]), 9283 // "RIFF" string literal in hex used for wav and avi 9284 'riff': toUint8([0x52, 0x49, 0x46, 0x46]), 9285 // "AVI" string literal in hex 9286 'avi': toUint8([0x41, 0x56, 0x49]), 9287 // "WAVE" string literal in hex 9288 'wav': toUint8([0x57, 0x41, 0x56, 0x45]), 9289 // "ftyp3g" string literal in hex 9290 '3gp': toUint8([0x66, 0x74, 0x79, 0x70, 0x33, 0x67]), 9291 // "ftyp" string literal in hex 9292 'mp4': toUint8([0x66, 0x74, 0x79, 0x70]), 9293 // "styp" string literal in hex 9294 'fmp4': toUint8([0x73, 0x74, 0x79, 0x70]), 9295 // "ftypqt" string literal in hex 9296 'mov': toUint8([0x66, 0x74, 0x79, 0x70, 0x71, 0x74]), 9297 // moov string literal in hex 9298 'moov': toUint8([0x6D, 0x6F, 0x6F, 0x76]), 9299 // moof string literal in hex 9300 'moof': toUint8([0x6D, 0x6F, 0x6F, 0x66]) 9301 }; 9302 var _isLikely = { 9303 aac: function aac(bytes) { 9304 var offset = getId3Offset(bytes); 9305 return bytesMatch(bytes, [0xFF, 0x10], { 9306 offset: offset, 9307 mask: [0xFF, 0x16] 9308 }); 9309 }, 9310 mp3: function mp3(bytes) { 9311 var offset = getId3Offset(bytes); 9312 return bytesMatch(bytes, [0xFF, 0x02], { 9313 offset: offset, 9314 mask: [0xFF, 0x06] 9315 }); 9316 }, 9317 webm: function webm(bytes) { 9318 var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is webm 9319 9320 return bytesMatch(docType, CONSTANTS.webm); 9321 }, 9322 mkv: function mkv(bytes) { 9323 var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is matroska 9324 9325 return bytesMatch(docType, CONSTANTS.matroska); 9326 }, 9327 mp4: function mp4(bytes) { 9328 // if this file is another base media file format, it is not mp4 9329 if (_isLikely['3gp'](bytes) || _isLikely.mov(bytes)) { 9330 return false; 9331 } // if this file starts with a ftyp or styp box its mp4 9332 9333 9334 if (bytesMatch(bytes, CONSTANTS.mp4, { 9335 offset: 4 9336 }) || bytesMatch(bytes, CONSTANTS.fmp4, { 9337 offset: 4 9338 })) { 9339 return true; 9340 } // if this file starts with a moof/moov box its mp4 9341 9342 9343 if (bytesMatch(bytes, CONSTANTS.moof, { 9344 offset: 4 9345 }) || bytesMatch(bytes, CONSTANTS.moov, { 9346 offset: 4 9347 })) { 9348 return true; 9349 } 9350 }, 9351 mov: function mov(bytes) { 9352 return bytesMatch(bytes, CONSTANTS.mov, { 9353 offset: 4 9354 }); 9355 }, 9356 '3gp': function gp(bytes) { 9357 return bytesMatch(bytes, CONSTANTS['3gp'], { 9358 offset: 4 9359 }); 9360 }, 9361 ac3: function ac3(bytes) { 9362 var offset = getId3Offset(bytes); 9363 return bytesMatch(bytes, CONSTANTS.ac3, { 9364 offset: offset 9365 }); 9366 }, 9367 ts: function ts(bytes) { 9368 if (bytes.length < 189 && bytes.length >= 1) { 9369 return bytes[0] === 0x47; 9370 } 9371 9372 var i = 0; // check the first 376 bytes for two matching sync bytes 9373 9374 while (i + 188 < bytes.length && i < 188) { 9375 if (bytes[i] === 0x47 && bytes[i + 188] === 0x47) { 9376 return true; 9377 } 9378 9379 i += 1; 9380 } 9381 9382 return false; 9383 }, 9384 flac: function flac(bytes) { 9385 var offset = getId3Offset(bytes); 9386 return bytesMatch(bytes, CONSTANTS.flac, { 9387 offset: offset 9388 }); 9389 }, 9390 ogg: function ogg(bytes) { 9391 return bytesMatch(bytes, CONSTANTS.ogg); 9392 }, 9393 avi: function avi(bytes) { 9394 return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.avi, { 9395 offset: 8 9396 }); 9397 }, 9398 wav: function wav(bytes) { 9399 return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.wav, { 9400 offset: 8 9401 }); 9402 }, 9403 'h264': function h264(bytes) { 9404 // find seq_parameter_set_rbsp 9405 return findH264Nal(bytes, 7, 3).length; 9406 }, 9407 'h265': function h265(bytes) { 9408 // find video_parameter_set_rbsp or seq_parameter_set_rbsp 9409 return findH265Nal(bytes, [32, 33], 3).length; 9410 } 9411 }; // get all the isLikely functions 9412 // but make sure 'ts' is above h264 and h265 9413 // but below everything else as it is the least specific 9414 9415 var isLikelyTypes = Object.keys(_isLikely) // remove ts, h264, h265 9416 .filter(function (t) { 9417 return t !== 'ts' && t !== 'h264' && t !== 'h265'; 9418 }) // add it back to the bottom 9419 .concat(['ts', 'h264', 'h265']); // make sure we are dealing with uint8 data. 9420 9421 isLikelyTypes.forEach(function (type) { 9422 var isLikelyFn = _isLikely[type]; 9423 9424 _isLikely[type] = function (bytes) { 9425 return isLikelyFn(toUint8(bytes)); 9426 }; 9427 }); // export after wrapping 9428 9429 var isLikely = _isLikely; // A useful list of file signatures can be found here 9430 // https://en.wikipedia.org/wiki/List_of_file_signatures 9431 9432 var detectContainerForBytes = function detectContainerForBytes(bytes) { 9433 bytes = toUint8(bytes); 9434 9435 for (var i = 0; i < isLikelyTypes.length; i++) { 9436 var type = isLikelyTypes[i]; 9437 9438 if (isLikely[type](bytes)) { 9439 return type; 9440 } 9441 } 9442 9443 return ''; 9444 }; // fmp4 is not a container 9445 9446 var isLikelyFmp4MediaSegment = function isLikelyFmp4MediaSegment(bytes) { 9447 return findBox(bytes, ['moof']).length > 0; 9448 }; 9449 9450 /
9450/ which will only happen if the request is complete. 9451 9452 const callbackOnCompleted = (request, cb) => { 9453 if (request.readyState === 4) { 9454 return cb(); 9455 } 9456 9457 return; 9458 }; 9459 9460 const containerRequest = (uri, xhr, cb) => { 9461 let bytes = []; 9462 let id3Offset; 9463 let finished = false; 9464 9465 const endRequestAndCallback = function (err, req, type, _bytes) { 9466 req.abort(); 9467 finished = true; 9468 return cb(err, req, type, _bytes); 9469 }; 9470 9471 const progressListener = function (error, request) { 9472 if (finished) { 9473 return; 9474 } 9475 9476 if (error) {
vendor: 6,044 bytes, lines 9476-9651
9476 9477 return endRequestAndCallback(error, request, '', bytes); 9478 } // grap the new part of content that was just downloaded 9479 9480 9481 const newPart = request.responseText.substring(bytes && bytes.byteLength || 0, request.responseText.length); // add that onto bytes 9482 9483 bytes = concatTypedArrays(bytes, stringToBytes(newPart, true)); 9484 id3Offset = id3Offset || getId3Offset(bytes); // we need at least 10 bytes to determine a type 9485 // or we need at least two bytes after an id3Offset 9486 9487 if (bytes.length < 10 || id3Offset && bytes.length < id3Offset + 2) { 9488 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 9489 } 9490 9491 const type = detectContainerForBytes(bytes); // if this looks like a ts segment but we don't have enough data 9492 // to see the second sync byte, wait until we have enough data 9493 // before declaring it ts 9494 9495 if (type === 'ts' && bytes.length < 188) { 9496 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 9497 } // this may be an unsynced ts segment 9498 // wait for 376 bytes before detecting no container 9499 9500 9501 if (!type && bytes.length < 376) { 9502 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 9503 } 9504 9505 return endRequestAndCallback(null, request, type, bytes); 9506 }; 9507 9508 const options = { 9509 uri, 9510 9511 beforeSend(request) { 9512 // this forces the browser to pass the bytes to us unprocessed 9513 request.overrideMimeType('text/plain; charset=x-user-defined'); 9514 request.addEventListener('progress', function ({ 9515 total, 9516 loaded 9517 }) { 9518 return callbackWrapper(request, null, { 9519 statusCode: request.status 9520 }, progressListener); 9521 }); 9522 } 9523 9524 }; 9525 const request = xhr(options, function (error, response) { 9526 return callbackWrapper(request, error, response, progressListener); 9527 }); 9528 return request; 9529 }; 9530 9531 const { 9532 EventTarget 9533 } = videojs__default["default"]; 9534 9535 const dashPlaylistUnchanged = function (a, b) { 9536 if (!isPlaylistUnchanged(a, b)) { 9537 return false; 9538 } // for dash the above check will often return true in scenarios where 9539 // the playlist actually has changed because mediaSequence isn't a 9540 // dash thing, and we often set it to 1. So if the playlists have the same amount 9541 // of segments we return true. 9542 // So for dash we need to make sure that the underlying segments are different. 9543 // if sidx changed then the playlists are different. 9544 9545 9546 if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) { 9547 return false; 9548 } else if (!a.sidx && b.sidx || a.sidx && !b.sidx) { 9549 return false; 9550 } // one or the other does not have segments 9551 // there was a change. 9552 9553 9554 if (a.segments && !b.segments || !a.segments && b.segments) { 9555 return false; 9556 } // neither has segments nothing changed 9557 9558 9559 if (!a.segments && !b.segments) { 9560 return true; 9561 } // check segments themselves 9562 9563 9564 for (let i = 0; i < a.segments.length; i++) { 9565 const aSegment = a.segments[i]; 9566 const bSegment = b.segments[i]; // if uris are different between segments there was a change 9567 9568 if (aSegment.uri !== bSegment.uri) { 9569 return false; 9570 } // neither segment has a byterange, there will be no byterange change. 9571 9572 9573 if (!aSegment.byterange && !bSegment.byterange) { 9574 continue; 9575 } 9576 9577 const aByterange = aSegment.byterange; 9578 const bByterange = bSegment.byterange; // if byterange only exists on one of the segments, there was a change. 9579 9580 if (aByterange && !bByterange || !aByterange && bByterange) { 9581 return false; 9582 } // if both segments have byterange with different offsets, there was a change. 9583 9584 9585 if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) { 9586 return false; 9587 } 9588 } // if everything was the same with segments, this is the same playlist. 9589 9590 9591 return true; 9592 }; 9593 /** 9594 * Use the representation IDs from the mpd object to create groupIDs, the NAME is set to mandatory representation 9595 * ID in the parser. This allows for continuous playout across periods with the same representation IDs 9596 * (continuous periods as defined in DASH-IF 3.2.12). This is assumed in the mpd-parser as well. If we want to support 9597 * periods without continuous playback this function may need modification as well as the parser. 9598 */ 9599 9600 9601 const dashGroupId = (type, group, label, playlist) => { 9602 // If the manifest somehow does not have an ID (non-dash compliant), use the label. 9603 const playlistId = playlist.attributes.NAME || label; 9604 return `placeholder-uri-${type}-${group}-${playlistId}`; 9605 }; 9606 /** 9607 * Parses the main XML string and updates playlist URI references. 9608 * 9609 * @param {Object} config 9610 * Object of arguments 9611 * @param {string} config.mainXml 9612 * The mpd XML 9613 * @param {string} config.srcUrl 9614 * The mpd URL 9615 * @param {Date} config.clientOffset 9616 * A time difference between server and client 9617 * @param {Object} config.sidxMapping 9618 * SIDX mappings for moof/mdat URIs and byte ranges 9619 * @return {Object} 9620 * The parsed mpd manifest object 9621 */ 9622 9623 9624 const parseMainXml = ({ 9625 mainXml, 9626 srcUrl, 9627 clientOffset, 9628 sidxMapping, 9629 previousManifest 9630 }) => { 9631 const manifest = parse(mainXml, { 9632 manifestUri: srcUrl, 9633 clientOffset, 9634 sidxMapping, 9635 previousManifest 9636 }); 9637 addPropertiesToMain(manifest, srcUrl, dashGroupId); 9638 return manifest; 9639 }; 9640 /** 9641 * Removes any mediaGroup labels that no longer exist in the newMain 9642 * 9643 * @param {Object} update 9644 * The previous mpd object being updated 9645 * @param {Object} newMain 9646 * The new mpd object 9647 */ 9648 9649 const removeOldMediaGroupLabels = (update, newMain) => { 9650 forEachMediaGroup$1(update, (properties, type, group, label) => { 9651 if (!
vendor: 16,699 bytes, lines 9651-10169
9651(label in newMain.mediaGroups[type][group])) { 9652 delete update.mediaGroups[type][group][label]; 9653 } 9654 }); 9655 }; 9656 /** 9657 * Returns a new main manifest that is the result of merging an updated main manifest 9658 * into the original version. 9659 * 9660 * @param {Object} oldMain 9661 * The old parsed mpd object 9662 * @param {Object} newMain 9663 * The updated parsed mpd object 9664 * @return {Object} 9665 * A new object representing the original main manifest with the updated media 9666 * playlists merged in 9667 */ 9668 9669 9670 const updateMain = (oldMain, newMain, sidxMapping) => { 9671 let noChanges = true; 9672 let update = merge$1(oldMain, { 9673 // These are top level properties that can be updated 9674 duration: newMain.duration, 9675 minimumUpdatePeriod: newMain.minimumUpdatePeriod, 9676 timelineStarts: newMain.timelineStarts 9677 }); // First update the playlists in playlist list 9678 9679 for (let i = 0; i < newMain.playlists.length; i++) { 9680 const playlist = newMain.playlists[i]; 9681 9682 if (playlist.sidx) { 9683 const sidxKey = generateSidxKey(playlist.sidx); // add sidx segments to the playlist if we have all the sidx info already 9684 9685 if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) { 9686 addSidxSegmentsToPlaylist$1(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri); 9687 } 9688 } 9689 9690 const playlistUpdate = updateMain$1(update, playlist, dashPlaylistUnchanged); 9691 9692 if (playlistUpdate) { 9693 update = playlistUpdate; 9694 noChanges = false; 9695 } 9696 } // Then update media group playlists 9697 9698 9699 forEachMediaGroup$1(newMain, (properties, type, group, label) => { 9700 if (properties.playlists && properties.playlists.length) { 9701 const id = properties.playlists[0].id; 9702 const playlistUpdate = updateMain$1(update, properties.playlists[0], dashPlaylistUnchanged); 9703 9704 if (playlistUpdate) { 9705 update = playlistUpdate; // add new mediaGroup label if it doesn't exist and assign the new mediaGroup. 9706 9707 if (!(label in update.mediaGroups[type][group])) { 9708 update.mediaGroups[type][group][label] = properties; 9709 } // update the playlist reference within media groups 9710 9711 9712 update.mediaGroups[type][group][label].playlists[0] = update.playlists[id]; 9713 noChanges = false; 9714 } 9715 } 9716 }); // remove mediaGroup labels and references that no longer exist in the newMain 9717 9718 removeOldMediaGroupLabels(update, newMain); 9719 9720 if (newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 9721 noChanges = false; 9722 } 9723 9724 if (noChanges) { 9725 return null; 9726 } 9727 9728 return update; 9729 }; // SIDX should be equivalent if the URI and byteranges of the SIDX match. 9730 // If the SIDXs have maps, the two maps should match, 9731 // both `a` and `b` missing SIDXs is considered matching. 9732 // If `a` or `b` but not both have a map, they aren't matching. 9733 9734 const equivalentSidx = (a, b) => { 9735 const neitherMap = Boolean(!a.map && !b.map); 9736 const equivalentMap = neitherMap || Boolean(a.map && b.map && a.map.byterange.offset === b.map.byterange.offset && a.map.byterange.length === b.map.byterange.length); 9737 return equivalentMap && a.uri === b.uri && a.byterange.offset === b.byterange.offset && a.byterange.length === b.byterange.length; 9738 }; // exported for testing 9739 9740 9741 const compareSidxEntry = (playlists, oldSidxMapping) => { 9742 const newSidxMapping = {}; 9743 9744 for (const id in playlists) { 9745 const playlist = playlists[id]; 9746 const currentSidxInfo = playlist.sidx; 9747 9748 if (currentSidxInfo) { 9749 const key = generateSidxKey(currentSidxInfo); 9750 9751 if (!oldSidxMapping[key]) { 9752 break; 9753 } 9754 9755 const savedSidxInfo = oldSidxMapping[key].sidxInfo; 9756 9757 if (equivalentSidx(savedSidxInfo, currentSidxInfo)) { 9758 newSidxMapping[key] = oldSidxMapping[key]; 9759 } 9760 } 9761 } 9762 9763 return newSidxMapping; 9764 }; 9765 /** 9766 * A function that filters out changed items as they need to be requested separately. 9767 * 9768 * The method is exported for testing 9769 * 9770 * @param {Object} main the parsed mpd XML returned via mpd-parser 9771 * @param {Object} oldSidxMapping the SIDX to compare against 9772 */ 9773 9774 const filterChangedSidxMappings = (main, oldSidxMapping) => { 9775 const videoSidx = compareSidxEntry(main.playlists, oldSidxMapping); 9776 let mediaGroupSidx = videoSidx; 9777 forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => { 9778 if (properties.playlists && properties.playlists.length) { 9779 const playlists = properties.playlists; 9780 mediaGroupSidx = merge$1(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping)); 9781 } 9782 }); 9783 return mediaGroupSidx; 9784 }; 9785 class DashPlaylistLoader extends EventTarget { 9786 // DashPlaylistLoader must accept either a src url or a playlist because subsequent 9787 // playlist loader setups from media groups will expect to be able to pass a playlist 9788 // (since there aren't external URLs to media playlists with DASH) 9789 constructor(srcUrlOrPlaylist, vhs, options = {}, mainPlaylistLoader) { 9790 super(); 9791 this.mainPlaylistLoader_ = mainPlaylistLoader || this; 9792 9793 if (!mainPlaylistLoader) { 9794 this.isMain_ = true; 9795 } 9796 9797 const { 9798 withCredentials = false 9799 } = options; 9800 this.vhs_ = vhs; 9801 this.withCredentials = withCredentials; 9802 this.addMetadataToTextTrack = options.addMetadataToTextTrack; 9803 9804 if (!srcUrlOrPlaylist) { 9805 throw new Error('A non-empty playlist URL or object is required'); 9806 } // event naming? 9807 9808 9809 this.on('minimumUpdatePeriod', () => { 9810 this.refreshXml_(); 9811 }); // live playlist staleness timeout 9812 9813 this.on('mediaupdatetimeout', () => { 9814 this.refreshMedia_(this.media().id); 9815 }); 9816 this.state = 'HAVE_NOTHING'; 9817 this.loadedPlaylists_ = {}; 9818 this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state 9819 // The mainPlaylistLoader will be created with a string 9820 9821 if (this.isMain_) { 9822 this.mainPlaylistLoader_.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes 9823 // once multi-period is refactored 9824 9825 this.mainPlaylistLoader_.sidxMapping_ = {}; 9826 } else { 9827 this.childPlaylist_ = srcUrlOrPlaylist; 9828 } 9829 } 9830 9831 requestErrored_(err, request, startingState) { 9832 // disposed 9833 if (!this.request) { 9834 return true; 9835 } // pending request is cleared 9836 9837 9838 this.request = null; 9839 9840 if (err) { 9841 // use the provided error object or create one 9842 // based on the request/response 9843 this.error = typeof err === 'object' && !(err instanceof Error) ? err : { 9844 status: request.status, 9845 message: 'DASH request error at URL: ' + request.uri, 9846 response: request.response, 9847 // MEDIA_ERR_NETWORK 9848 code: 2 9849 }; 9850 9851 if (startingState) { 9852 this.state = startingState; 9853 } 9854 9855 this.trigger('error'); 9856 return true; 9857 } 9858 } 9859 /** 9860 * Verify that the container of the sidx segment can be parsed 9861 * and if it can, get and parse that segment. 9862 */ 9863 9864 9865 addSidxSegments_(playlist, startingState, cb) { 9866 const sidxKey = playlist.sidx && generateSidxKey(playlist.sidx); // playlist lacks sidx or sidx segments were added to this playlist already. 9867 9868 if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) { 9869 // keep this function async 9870 this.mediaRequest_ = window.setTimeout(() => cb(false), 0); 9871 return; 9872 } // resolve the segment URL relative to the playlist 9873 9874 9875 const uri = resolveManifestRedirect(playlist.sidx.resolvedUri); 9876 9877 const fin = (err, request) => { 9878 if (this.requestErrored_(err, request, startingState)) { 9879 return; 9880 } 9881 9882 const sidxMapping = this.mainPlaylistLoader_.sidxMapping_; 9883 let sidx; 9884 9885 try { 9886 sidx = parseSidx_1(toUint8(request.response).subarray(8)); 9887 } catch (e) { 9888 // sidx parsing failed. 9889 this.requestErrored_(e, request, startingState); 9890 return; 9891 } 9892 9893 sidxMapping[sidxKey] = { 9894 sidxInfo: playlist.sidx, 9895 sidx 9896 }; 9897 addSidxSegmentsToPlaylist$1(playlist, sidx, playlist.sidx.resolvedUri); 9898 return cb(true); 9899 }; 9900 9901 this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => { 9902 if (err) { 9903 return fin(err, request); 9904 } 9905 9906 if (!container || container !== 'mp4') { 9907 return fin({ 9908 status: request.status, 9909 message: `Unsupported ${container || 'unknown'} container type for sidx segment at URL: ${uri}`, 9910 // response is just bytes in this case 9911 // but we really don't want to return that. 9912 response: '', 9913 playlist, 9914 internal: true, 9915 playlistExclusionDuration: Infinity, 9916 // MEDIA_ERR_NETWORK 9917 code: 2 9918 }, request); 9919 } // if we already downloaded the sidx bytes in the container request, use them 9920 9921 9922 const { 9923 offset, 9924 length 9925 } = playlist.sidx.byterange; 9926 9927 if (bytes.length >= length + offset) { 9928 return fin(err, { 9929 response: bytes.subarray(offset, offset + length), 9930 status: request.status, 9931 uri: request.uri 9932 }); 9933 } // otherwise request sidx bytes 9934 9935 9936 this.request = this.vhs_.xhr({ 9937 uri, 9938 responseType: 'arraybuffer', 9939 headers: segmentXhrHeaders({ 9940 byterange: playlist.sidx.byterange 9941 }) 9942 }, fin); 9943 }); 9944 } 9945 9946 dispose() { 9947 this.trigger('dispose'); 9948 this.stopRequest(); 9949 this.loadedPlaylists_ = {}; 9950 window.clearTimeout(this.minimumUpdatePeriodTimeout_); 9951 window.clearTimeout(this.mediaRequest_); 9952 window.clearTimeout(this.mediaUpdateTimeout); 9953 this.mediaUpdateTimeout = null; 9954 this.mediaRequest_ = null; 9955 this.minimumUpdatePeriodTimeout_ = null; 9956 9957 if (this.mainPlaylistLoader_.createMupOnMedia_) { 9958 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 9959 this.mainPlaylistLoader_.createMupOnMedia_ = null; 9960 } 9961 9962 this.off(); 9963 } 9964 9965 hasPendingRequest() { 9966 return this.request || this.mediaRequest_; 9967 } 9968 9969 stopRequest() { 9970 if (this.request) { 9971 const oldRequest = this.request; 9972 this.request = null; 9973 oldRequest.onreadystatechange = null; 9974 oldRequest.abort(); 9975 } 9976 } 9977 9978 media(playlist) { 9979 // getter 9980 if (!playlist) { 9981 return this.media_; 9982 } // setter 9983 9984 9985 if (this.state === 'HAVE_NOTHING') { 9986 throw new Error('Cannot switch media playlist from ' + this.state); 9987 } 9988 9989 const startingState = this.state; // find the playlist object if the target playlist has been specified by URI 9990 9991 if (typeof playlist === 'string') { 9992 if (!this.mainPlaylistLoader_.main.playlists[playlist]) { 9993 throw new Error('Unknown playlist URI: ' + playlist); 9994 } 9995 9996 playlist = this.mainPlaylistLoader_.main.playlists[playlist]; 9997 } 9998 9999 const mediaChange = !this.media_ || playlist.id !== this.media_.id; // switch to previously loaded playlists immediately 10000 10001 if (mediaChange && this.loadedPlaylists_[playlist.id] && this.loadedPlaylists_[playlist.id].endList) { 10002 this.state = 'HAVE_METADATA'; 10003 this.media_ = playlist; // trigger media change if the active media has been updated 10004 10005 if (mediaChange) { 10006 this.trigger('mediachanging'); 10007 this.trigger('mediachange'); 10008 } 10009 10010 return; 10011 } // switching to the active playlist is a no-op 10012 10013 10014 if (!mediaChange) { 10015 return; 10016 } // switching from an already loaded playlist 10017 10018 10019 if (this.media_) { 10020 this.trigger('mediachanging'); 10021 } 10022 10023 this.addSidxSegments_(playlist, startingState, sidxChanged => { 10024 // everything is ready just continue to haveMetadata 10025 this.haveMetadata({ 10026 startingState, 10027 playlist 10028 }); 10029 }); 10030 } 10031 10032 haveMetadata({ 10033 startingState, 10034 playlist 10035 }) { 10036 this.state = 'HAVE_METADATA'; 10037 this.loadedPlaylists_[playlist.id] = playlist; 10038 this.mediaRequest_ = null; // This will trigger loadedplaylist 10039 10040 this.refreshMedia_(playlist.id); // fire loadedmetadata the first time a media playlist is loaded 10041 // to resolve setup of media groups 10042 10043 if (startingState === 'HAVE_MAIN_MANIFEST') { 10044 this.trigger('loadedmetadata'); 10045 } else { 10046 // trigger media change if the active media has been updated 10047 this.trigger('mediachange'); 10048 } 10049 } 10050 10051 pause() { 10052 if (this.mainPlaylistLoader_.createMupOnMedia_) { 10053 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 10054 this.mainPlaylistLoader_.createMupOnMedia_ = null; 10055 } 10056 10057 this.stopRequest(); 10058 window.clearTimeout(this.mediaUpdateTimeout); 10059 this.mediaUpdateTimeout = null; 10060 10061 if (this.isMain_) { 10062 window.clearTimeout(this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_); 10063 this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_ = null; 10064 } 10065 10066 if (this.state === 'HAVE_NOTHING') { 10067 // If we pause the loader before any data has been retrieved, its as if we never 10068 // started, so reset to an unstarted state. 10069 this.started = false; 10070 } 10071 } 10072 10073 load(isFinalRendition) { 10074 window.clearTimeout(this.mediaUpdateTimeout); 10075 this.mediaUpdateTimeout = null; 10076 const media = this.media(); 10077 10078 if (isFinalRendition) { 10079 const delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 10080 this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay); 10081 return; 10082 } // because the playlists are internal to the manifest, load should either load the 10083 // main manifest, or do nothing but trigger an event 10084 10085 10086 if (!this.started) { 10087 this.start(); 10088 return; 10089 } 10090 10091 if (media && !media.endList) { 10092 // Check to see if this is the main loader and the MUP was cleared (this happens 10093 // when the loader was paused). `media` should be set at this point since one is always 10094 // set during `start()`. 10095 if (this.isMain_ && !this.minimumUpdatePeriodTimeout_) { 10096 // Trigger minimumUpdatePeriod to refresh the main manifest 10097 this.trigger('minimumUpdatePeriod'); // Since there was no prior minimumUpdatePeriodTimeout it should be recreated 10098 10099 this.updateMinimumUpdatePeriodTimeout_(); 10100 } 10101 10102 this.trigger('mediaupdatetimeout'); 10103 } else { 10104 this.trigger('loadedplaylist'); 10105 } 10106 } 10107 10108 start() { 10109 this.started = true; // We don't need to request the main manifest again 10110 // Call this asynchronously to match the xhr request behavior below 10111 10112 if (!this.isMain_) { 10113 this.mediaRequest_ = window.setTimeout(() => this.haveMain_(), 0); 10114 return; 10115 } 10116 10117 this.requestMain_((req, mainChanged) => { 10118 this.haveMain_(); 10119 10120 if (!this.hasPendingRequest() && !this.media_) { 10121 this.media(this.mainPlaylistLoader_.main.playlists[0]); 10122 } 10123 }); 10124 } 10125 10126 requestMain_(cb) { 10127 this.request = this.vhs_.xhr({ 10128 uri: this.mainPlaylistLoader_.srcUrl, 10129 withCredentials: this.withCredentials 10130 }, (error, req) => { 10131 if (this.requestErrored_(error, req)) { 10132 if (this.state === 'HAVE_NOTHING') { 10133 this.started = false; 10134 } 10135 10136 return; 10137 } 10138 10139 const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_; 10140 this.mainPlaylistLoader_.mainXml_ = req.responseText; 10141 10142 if (req.responseHeaders && req.responseHeaders.date) { 10143 this.mainLoaded_ = Date.parse(req.responseHeaders.date); 10144 } else { 10145 this.mainLoaded_ = Date.now(); 10146 } 10147 10148 this.mainPlaylistLoader_.srcUrl = resolveManifestRedirect(this.mainPlaylistLoader_.srcUrl, req); 10149 10150 if (mainChanged) { 10151 this.handleMain_(); 10152 this.syncClientServerClock_(() => { 10153 return cb(req, mainChanged); 10154 }); 10155 return; 10156 } 10157 10158 return cb(req, mainChanged); 10159 }); 10160 } 10161 /** 10162 * Parses the main xml for UTCTiming node to sync the client clock to the server 10163 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 10164 * 10165 * @param {Function} done 10166 * Function to call when clock sync has completed 10167 */ 10168 10169
vendor: 4,234 bytes, lines 10170-10288
10170 syncClientServerClock_(done) { 10171 const utcTiming = parseUTCTiming(this.mainPlaylistLoader_.mainXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the 10172 // server clock 10173 10174 if (utcTiming === null) { 10175 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 10176 return done(); 10177 } 10178 10179 if (utcTiming.method === 'DIRECT') { 10180 this.mainPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now(); 10181 return done(); 10182 } 10183 10184 this.request = this.vhs_.xhr({ 10185 uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value), 10186 method: utcTiming.method, 10187 withCredentials: this.withCredentials 10188 }, (error, req) => { 10189 // disposed 10190 if (!this.request) { 10191 return; 10192 } 10193 10194 if (error) { 10195 // sync request failed, fall back to using date header from mpd 10196 // TODO: log warning 10197 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 10198 return done(); 10199 } 10200 10201 let serverTime; 10202 10203 if (utcTiming.method === 'HEAD') { 10204 if (!req.responseHeaders || !req.responseHeaders.date) { 10205 // expected date header not preset, fall back to using date header from mpd 10206 // TODO: log warning 10207 serverTime = this.mainLoaded_; 10208 } else { 10209 serverTime = Date.parse(req.responseHeaders.date); 10210 } 10211 } else { 10212 serverTime = Date.parse(req.responseText); 10213 } 10214 10215 this.mainPlaylistLoader_.clientOffset_ = serverTime - Date.now(); 10216 done(); 10217 }); 10218 } 10219 10220 haveMain_() { 10221 this.state = 'HAVE_MAIN_MANIFEST'; 10222 10223 if (this.isMain_) { 10224 // We have the main playlist at this point, so 10225 // trigger this to allow PlaylistController 10226 // to make an initial playlist selection 10227 this.trigger('loadedplaylist'); 10228 } else if (!this.media_) { 10229 // no media playlist was specifically selected so select 10230 // the one the child playlist loader was created with 10231 this.media(this.childPlaylist_); 10232 } 10233 } 10234 10235 handleMain_() { 10236 // clear media request 10237 this.mediaRequest_ = null; 10238 const oldMain = this.mainPlaylistLoader_.main; 10239 let newMain = parseMainXml({ 10240 mainXml: this.mainPlaylistLoader_.mainXml_, 10241 srcUrl: this.mainPlaylistLoader_.srcUrl, 10242 clientOffset: this.mainPlaylistLoader_.clientOffset_, 10243 sidxMapping: this.mainPlaylistLoader_.sidxMapping_, 10244 previousManifest: oldMain 10245 }); // if we have an old main to compare the new main against 10246 10247 if (oldMain) { 10248 newMain = updateMain(oldMain, newMain, this.mainPlaylistLoader_.sidxMapping_); 10249 } // only update main if we have a new main 10250 10251 10252 this.mainPlaylistLoader_.main = newMain ? newMain : oldMain; 10253 const location = this.mainPlaylistLoader_.main.locations && this.mainPlaylistLoader_.main.locations[0]; 10254 10255 if (location && location !== this.mainPlaylistLoader_.srcUrl) { 10256 this.mainPlaylistLoader_.srcUrl = location; 10257 } 10258 10259 if (!oldMain || newMain && newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 10260 this.updateMinimumUpdatePeriodTimeout_(); 10261 } 10262 10263 this.addEventStreamToMetadataTrack_(newMain); 10264 return Boolean(newMain); 10265 } 10266 10267 updateMinimumUpdatePeriodTimeout_() { 10268 const mpl = this.mainPlaylistLoader_; // cancel any pending creation of mup on media 10269 // a new one will be added if needed. 10270 10271 if (mpl.createMupOnMedia_) { 10272 mpl.off('loadedmetadata', mpl.createMupOnMedia_); 10273 mpl.createMupOnMedia_ = null; 10274 } // clear any pending timeouts 10275 10276 10277 if (mpl.minimumUpdatePeriodTimeout_) { 10278 window.clearTimeout(mpl.minimumUpdatePeriodTimeout_); 10279 mpl.minimumUpdatePeriodTimeout_ = null; 10280 } 10281 10282 let mup = mpl.main && mpl.main.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current 10283 // MPD has no future validity, so a new one will need to be acquired when new 10284 // media segments are to be made available. Thus, we use the target duration 10285 // in this case 10286 10287 if (mup === 0) { 10288 if (mpl.media()) {
vendor: 12,596 bytes, lines 10289-10689
10289 mup = mpl.media().targetDuration * 1000; 10290 } else { 10291 mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_; 10292 mpl.one('loadedmetadata', mpl.createMupOnMedia_); 10293 } 10294 } // if minimumUpdatePeriod is invalid or <= zero, which 10295 // can happen when a live video becomes VOD. skip timeout 10296 // creation. 10297 10298 10299 if (typeof mup !== 'number' || mup <= 0) { 10300 if (mup < 0) { 10301 this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`); 10302 } 10303 10304 return; 10305 } 10306 10307 this.createMUPTimeout_(mup); 10308 } 10309 10310 createMUPTimeout_(mup) { 10311 const mpl = this.mainPlaylistLoader_; 10312 mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { 10313 mpl.minimumUpdatePeriodTimeout_ = null; 10314 mpl.trigger('minimumUpdatePeriod'); 10315 mpl.createMUPTimeout_(mup); 10316 }, mup); 10317 } 10318 /** 10319 * Sends request to refresh the main xml and updates the parsed main manifest 10320 */ 10321 10322 10323 refreshXml_() { 10324 this.requestMain_((req, mainChanged) => { 10325 if (!mainChanged) { 10326 return; 10327 } 10328 10329 if (this.media_) { 10330 this.media_ = this.mainPlaylistLoader_.main.playlists[this.media_.id]; 10331 } // This will filter out updated sidx info from the mapping 10332 10333 10334 this.mainPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.sidxMapping_); 10335 this.addSidxSegments_(this.media(), this.state, sidxChanged => { 10336 // TODO: do we need to reload the current playlist? 10337 this.refreshMedia_(this.media().id); 10338 }); 10339 }); 10340 } 10341 /** 10342 * Refreshes the media playlist by re-parsing the main xml and updating playlist 10343 * references. If this is an alternate loader, the updated parsed manifest is retrieved 10344 * from the main loader. 10345 */ 10346 10347 10348 refreshMedia_(mediaID) { 10349 if (!mediaID) { 10350 throw new Error('refreshMedia_ must take a media id'); 10351 } // for main we have to reparse the main xml 10352 // to re-create segments based on current timing values 10353 // which may change media. We only skip updating the main manifest 10354 // if this is the first time this.media_ is being set. 10355 // as main was just parsed in that case. 10356 10357 10358 if (this.media_ && this.isMain_) { 10359 this.handleMain_(); 10360 } 10361 10362 const playlists = this.mainPlaylistLoader_.main.playlists; 10363 const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID]; 10364 10365 if (mediaChanged) { 10366 this.media_ = playlists[mediaID]; 10367 } else { 10368 this.trigger('playlistunchanged'); 10369 } 10370 10371 if (!this.mediaUpdateTimeout) { 10372 const createMediaUpdateTimeout = () => { 10373 if (this.media().endList) { 10374 return; 10375 } 10376 10377 this.mediaUpdateTimeout = window.setTimeout(() => { 10378 this.trigger('mediaupdatetimeout'); 10379 createMediaUpdateTimeout(); 10380 }, refreshDelay(this.media(), Boolean(mediaChanged))); 10381 }; 10382 10383 createMediaUpdateTimeout(); 10384 } 10385 10386 this.trigger('loadedplaylist'); 10387 } 10388 /** 10389 * Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track. 10390 * 10391 * @param {manifest} newMain the newly parsed manifest 10392 */ 10393 10394 10395 addEventStreamToMetadataTrack_(newMain) { 10396 // Only add new event stream metadata if we have a new manifest. 10397 if (newMain && this.mainPlaylistLoader_.main.eventStream) { 10398 // convert EventStream to ID3-like data. 10399 const metadataArray = this.mainPlaylistLoader_.main.eventStream.map(eventStreamNode => { 10400 return { 10401 cueTime: eventStreamNode.start, 10402 frames: [{ 10403 data: eventStreamNode.messageData 10404 }] 10405 }; 10406 }); 10407 this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration); 10408 } 10409 } 10410 /** 10411 * Returns the key ID set from a playlist 10412 * 10413 * @param {playlist} playlist to fetch the key ID set from. 10414 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 10415 */ 10416 10417 10418 getKeyIdSet(playlist) { 10419 if (playlist.contentProtection) { 10420 const keyIds = new Set(); 10421 10422 for (const keysystem in playlist.contentProtection) { 10423 const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID']; 10424 10425 if (defaultKID) { 10426 // DASH keyIds are separated by dashes. 10427 keyIds.add(defaultKID.replace(/-/g, '').toLowerCase()); 10428 } 10429 } 10430 10431 return keyIds; 10432 } 10433 } 10434 10435 } 10436 10437 var Config = { 10438 GOAL_BUFFER_LENGTH: 30, 10439 MAX_GOAL_BUFFER_LENGTH: 60, 10440 BACK_BUFFER_LENGTH: 30, 10441 GOAL_BUFFER_LENGTH_RATE: 1, 10442 // 0.5 MB/s 10443 INITIAL_BANDWIDTH: 4194304, 10444 // A fudge factor to apply to advertised playlist bitrates to account for 10445 // temporary flucations in client bandwidth 10446 BANDWIDTH_VARIANCE: 1.2, 10447 // How much of the buffer must be filled before we consider upswitching 10448 BUFFER_LOW_WATER_LINE: 0, 10449 MAX_BUFFER_LOW_WATER_LINE: 30, 10450 // TODO: Remove this when experimentalBufferBasedABR is removed 10451 EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16, 10452 BUFFER_LOW_WATER_LINE_RATE: 1, 10453 // If the buffer is greater than the high water line, we won't switch down 10454 BUFFER_HIGH_WATER_LINE: 30 10455 }; 10456 10457 const stringToArrayBuffer = string => { 10458 const view = new Uint8Array(new ArrayBuffer(string.length)); 10459 10460 for (let i = 0; i < string.length; i++) { 10461 view[i] = string.charCodeAt(i); 10462 } 10463 10464 return view.buffer; 10465 }; 10466 10467 /* global Blob, BlobBuilder, Worker */ 10468 // unify worker interface 10469 const browserWorkerPolyFill = function (workerObj) { 10470 // node only supports on/off 10471 workerObj.on = workerObj.addEventListener; 10472 workerObj.off = workerObj.removeEventListener; 10473 return workerObj; 10474 }; 10475 10476 const createObjectURL = function (str) { 10477 try { 10478 return URL.createObjectURL(new Blob([str], { 10479 type: 'application/javascript' 10480 })); 10481 } catch (e) { 10482 const blob = new BlobBuilder(); 10483 blob.append(str); 10484 return URL.createObjectURL(blob.getBlob()); 10485 } 10486 }; 10487 10488 const factory = function (code) { 10489 return function () { 10490 const objectUrl = createObjectURL(code); 10491 const worker = browserWorkerPolyFill(new Worker(objectUrl)); 10492 worker.objURL = objectUrl; 10493 const terminate = worker.terminate; 10494 worker.on = worker.addEventListener; 10495 worker.off = worker.removeEventListener; 10496 10497 worker.terminate = function () { 10498 URL.revokeObjectURL(objectUrl); 10499 return terminate.call(this); 10500 }; 10501 10502 return worker; 10503 }; 10504 }; 10505 const transform = function (code) { 10506 return `var browserWorkerPolyFill = ${browserWorkerPolyFill.toString()};\n` + 'browserWorkerPolyFill(self);\n' + code; 10507 }; 10508 10509 const getWorkerString = function (fn) { 10510 return fn.toString().replace(/^function.+?{/, '').slice(0, -1); 10511 }; 10512 10513 /* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */ 10514 const workerCode$1 = transform(getWorkerString(function () { 10515 10516 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 10517 /** 10518 * mux.js 10519 * 10520 * Copyright (c) Brightcove 10521 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 10522 * 10523 * A lightweight readable stream implemention that handles event dispatching. 10524 * Objects that inherit from streams should call init in their constructors. 10525 */ 10526 10527 var Stream$8 = function () { 10528 this.init = function () { 10529 var listeners = {}; 10530 /** 10531 * Add a listener for a specified event type. 10532 * @param type {string} the event name 10533 * @param listener {function} the callback to be invoked when an event of 10534 * the specified type occurs 10535 */ 10536 10537 this.on = function (type, listener) { 10538 if (!listeners[type]) { 10539 listeners[type] = []; 10540 } 10541 10542 listeners[type] = listeners[type].concat(listener); 10543 }; 10544 /** 10545 * Remove a listener for a specified event type. 10546 * @param type {string} the event name 10547 * @param listener {function} a function previously registered for this 10548 * type of event through `on` 10549 */ 10550 10551 10552 this.off = function (type, listener) { 10553 var index; 10554 10555 if (!listeners[type]) { 10556 return false; 10557 } 10558 10559 index = listeners[type].indexOf(listener); 10560 listeners[type] = listeners[type].slice(); 10561 listeners[type].splice(index, 1); 10562 return index > -1; 10563 }; 10564 /** 10565 * Trigger an event of the specified type on this stream. Any additional 10566 * arguments to this function are passed as parameters to event listeners. 10567 * @param type {string} the event name 10568 */ 10569 10570 10571 this.trigger = function (type) { 10572 var callbacks, i, length, args; 10573 callbacks = listeners[type]; 10574 10575 if (!callbacks) { 10576 return; 10577 } // Slicing the arguments on every invocation of this method 10578 // can add a significant amount of overhead. Avoid the 10579 // intermediate object creation for the common case of a 10580 // single callback argument 10581 10582 10583 if (arguments.length === 2) { 10584 length = callbacks.length; 10585 10586 for (i = 0; i < length; ++i) { 10587 callbacks[i].call(this, arguments[1]); 10588 } 10589 } else { 10590 args = []; 10591 i = arguments.length; 10592 10593 for (i = 1; i < arguments.length; ++i) { 10594 args.push(arguments[i]); 10595 } 10596 10597 length = callbacks.length; 10598 10599 for (i = 0; i < length; ++i) { 10600 callbacks[i].apply(this, args); 10601 } 10602 } 10603 }; 10604 /** 10605 * Destroys the stream and cleans up. 10606 */ 10607 10608 10609 this.dispose = function () { 10610 listeners = {}; 10611 }; 10612 }; 10613 }; 10614 /** 10615 * Forwards all `data` events on this stream to the destination stream. The 10616 * destination stream should provide a method `push` to receive the data 10617 * events as they arrive. 10618 * @param destination {stream} the stream that will receive all `data` events 10619 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 10620 * when the current stream emits a 'done' event 10621 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 10622 */ 10623 10624 10625 Stream$8.prototype.pipe = function (destination) { 10626 this.on('data', function (data) { 10627 destination.push(data); 10628 }); 10629 this.on('done', function (flushSource) { 10630 destination.flush(flushSource); 10631 }); 10632 this.on('partialdone', function (flushSource) { 10633 destination.partialFlush(flushSource); 10634 }); 10635 this.on('endedtimeline', function (flushSource) { 10636 destination.endTimeline(flushSource); 10637 }); 10638 this.on('reset', function (flushSource) { 10639 destination.reset(flushSource); 10640 }); 10641 return destination; 10642 }; // Default stream functions that are expected to be overridden to perform 10643 // actual work. These are provided by the prototype as a sort of no-op 10644 // implementation so that we don't have to check for their existence in the 10645 // `pipe` function above. 10646 10647 10648 Stream$8.prototype.push = function (data) { 10649 this.trigger('data', data); 10650 }; 10651 10652 Stream$8.prototype.flush = function (flushSource) { 10653 this.trigger('done', flushSource); 10654 }; 10655 10656 Stream$8.prototype.partialFlush = function (flushSource) { 10657 this.trigger('partialdone', flushSource); 10658 }; 10659 10660 Stream$8.prototype.endTimeline = function (flushSource) { 10661 this.trigger('endedtimeline', flushSource); 10662 }; 10663 10664 Stream$8.prototype.reset = function (flushSource) { 10665 this.trigger('reset', flushSource); 10666 }; 10667 10668 var stream = Stream$8; 10669 var MAX_UINT32$1 = Math.pow(2, 32); 10670 10671 var getUint64$3 = function (uint8) { 10672 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 10673 var value; 10674 10675 if (dv.getBigUint64) { 10676 value = dv.getBigUint64(0); 10677 10678 if (value < Number.MAX_SAFE_INTEGER) { 10679 return Number(value); 10680 } 10681 10682 return value; 10683 } 10684 10685 return dv.getUint32(0) * MAX_UINT32$1 + dv.getUint32(4); 10686 }; 10687 10688 var numbers = { 10689 getUint64: getUint64$3
vendor: 99,331 bytes, lines 10689-13696
10689, 10690 MAX_UINT32: MAX_UINT32$1 10691 }; 10692 /** 10693 * mux.js 10694 * 10695 * Copyright (c) Brightcove 10696 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 10697 * 10698 * Functions that generate fragmented MP4s suitable for use with Media 10699 * Source Extensions. 10700 */ 10701 10702 var MAX_UINT32 = numbers.MAX_UINT32; 10703 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 10704 10705 (function () { 10706 var i; 10707 types = { 10708 avc1: [], 10709 // codingname 10710 avcC: [], 10711 btrt: [], 10712 dinf: [], 10713 dref: [], 10714 esds: [], 10715 ftyp: [], 10716 hdlr: [], 10717 mdat: [], 10718 mdhd: [], 10719 mdia: [], 10720 mfhd: [], 10721 minf: [], 10722 moof: [], 10723 moov: [], 10724 mp4a: [], 10725 // codingname 10726 mvex: [], 10727 mvhd: [], 10728 pasp: [], 10729 sdtp: [], 10730 smhd: [], 10731 stbl: [], 10732 stco: [], 10733 stsc: [], 10734 stsd: [], 10735 stsz: [], 10736 stts: [], 10737 styp: [], 10738 tfdt: [], 10739 tfhd: [], 10740 traf: [], 10741 trak: [], 10742 trun: [], 10743 trex: [], 10744 tkhd: [], 10745 vmhd: [] 10746 }; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 10747 // don't throw an error 10748 10749 if (typeof Uint8Array === 'undefined') { 10750 return; 10751 } 10752 10753 for (i in types) { 10754 if (types.hasOwnProperty(i)) { 10755 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 10756 } 10757 } 10758 10759 MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]); 10760 AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]); 10761 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 10762 VIDEO_HDLR = new Uint8Array([0x00, // version 0 10763 0x00, 0x00, 0x00, // flags 10764 0x00, 0x00, 0x00, 0x00, // pre_defined 10765 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 10766 0x00, 0x00, 0x00, 0x00, // reserved 10767 0x00, 0x00, 0x00, 0x00, // reserved 10768 0x00, 0x00, 0x00, 0x00, // reserved 10769 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 10770 ]); 10771 AUDIO_HDLR = new Uint8Array([0x00, // version 0 10772 0x00, 0x00, 0x00, // flags 10773 0x00, 0x00, 0x00, 0x00, // pre_defined 10774 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 10775 0x00, 0x00, 0x00, 0x00, // reserved 10776 0x00, 0x00, 0x00, 0x00, // reserved 10777 0x00, 0x00, 0x00, 0x00, // reserved 10778 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 10779 ]); 10780 HDLR_TYPES = { 10781 video: VIDEO_HDLR, 10782 audio: AUDIO_HDLR 10783 }; 10784 DREF = new Uint8Array([0x00, // version 0 10785 0x00, 0x00, 0x00, // flags 10786 0x00, 0x00, 0x00, 0x01, // entry_count 10787 0x00, 0x00, 0x00, 0x0c, // entry_size 10788 0x75, 0x72, 0x6c, 0x20, // 'url' type 10789 0x00, // version 0 10790 0x00, 0x00, 0x01 // entry_flags 10791 ]); 10792 SMHD = new Uint8Array([0x00, // version 10793 0x00, 0x00, 0x00, // flags 10794 0x00, 0x00, // balance, 0 means centered 10795 0x00, 0x00 // reserved 10796 ]); 10797 STCO = new Uint8Array([0x00, // version 10798 0x00, 0x00, 0x00, // flags 10799 0x00, 0x00, 0x00, 0x00 // entry_count 10800 ]); 10801 STSC = STCO; 10802 STSZ = new Uint8Array([0x00, // version 10803 0x00, 0x00, 0x00, // flags 10804 0x00, 0x00, 0x00, 0x00, // sample_size 10805 0x00, 0x00, 0x00, 0x00 // sample_count 10806 ]); 10807 STTS = STCO; 10808 VMHD = new Uint8Array([0x00, // version 10809 0x00, 0x00, 0x01, // flags 10810 0x00, 0x00, // graphicsmode 10811 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 10812 ]); 10813 })(); 10814 10815 box = function (type) { 10816 var payload = [], 10817 size = 0, 10818 i, 10819 result, 10820 view; 10821 10822 for (i = 1; i < arguments.length; i++) { 10823 payload.push(arguments[i]); 10824 } 10825 10826 i = payload.length; // calculate the total size we need to allocate 10827 10828 while (i--) { 10829 size += payload[i].byteLength; 10830 } 10831 10832 result = new Uint8Array(size + 8); 10833 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 10834 view.setUint32(0, result.byteLength); 10835 result.set(type, 4); // copy the payload into the result 10836 10837 for (i = 0, size = 8; i < payload.length; i++) { 10838 result.set(payload[i], size); 10839 size += payload[i].byteLength; 10840 } 10841 10842 return result; 10843 }; 10844 10845 dinf = function () { 10846 return box(types.dinf, box(types.dref, DREF)); 10847 }; 10848 10849 esds = function (track) { 10850 return box(types.esds, new Uint8Array([0x00, // version 10851 0x00, 0x00, 0x00, // flags 10852 // ES_Descriptor 10853 0x03, // tag, ES_DescrTag 10854 0x19, // length 10855 0x00, 0x00, // ES_ID 10856 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority 10857 // DecoderConfigDescriptor 10858 0x04, // tag, DecoderConfigDescrTag 10859 0x11, // length 10860 0x40, // object type 10861 0x15, // streamType 10862 0x00, 0x06, 0x00, // bufferSizeDB 10863 0x00, 0x00, 0xda, 0xc0, // maxBitrate 10864 0x00, 0x00, 0xda, 0xc0, // avgBitrate 10865 // DecoderSpecificInfo 10866 0x05, // tag, DecoderSpecificInfoTag 10867 0x02, // length 10868 // ISO/IEC 14496-3, AudioSpecificConfig 10869 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 10870 track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig 10871 ])); 10872 }; 10873 10874 ftyp = function () { 10875 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 10876 }; 10877 10878 hdlr = function (type) { 10879 return box(types.hdlr, HDLR_TYPES[type]); 10880 }; 10881 10882 mdat = function (data) { 10883 return box(types.mdat, data); 10884 }; 10885 10886 mdhd = function (track) { 10887 var result = new Uint8Array([0x00, // version 0 10888 0x00, 0x00, 0x00, // flags 10889 0x00, 0x00, 0x00, 0x02, // creation_time 10890 0x00, 0x00, 0x00, 0x03, // modification_time 10891 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 10892 track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration 10893 0x55, 0xc4, // 'und' language (undetermined) 10894 0x00, 0x00]); // Use the sample rate from the track metadata, when it is 10895 // defined. The sample rate can be parsed out of an ADTS header, for 10896 // instance. 10897 10898 if (track.samplerate) { 10899 result[12] = track.samplerate >>> 24 & 0xFF; 10900 result[13] = track.samplerate >>> 16 & 0xFF; 10901 result[14] = track.samplerate >>> 8 & 0xFF; 10902 result[15] = track.samplerate & 0xFF; 10903 } 10904 10905 return box(types.mdhd, result); 10906 }; 10907 10908 mdia = function (track) { 10909 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 10910 }; 10911 10912 mfhd = function (sequenceNumber) { 10913 return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags 10914 (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number 10915 ])); 10916 }; 10917 10918 minf = function (track) { 10919 return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track)); 10920 }; 10921 10922 moof = function (sequenceNumber, tracks) { 10923 var trackFragments = [], 10924 i = tracks.length; // build traf boxes for each track fragment 10925 10926 while (i--) { 10927 trackFragments[i] = traf(tracks[i]); 10928 } 10929 10930 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 10931 }; 10932 /** 10933 * Returns a movie box. 10934 * @param tracks {array} the tracks associated with this movie 10935 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 10936 */ 10937 10938 10939 moov = function (tracks) { 10940 var i = tracks.length, 10941 boxes = []; 10942 10943 while (i--) { 10944 boxes[i] = trak(tracks[i]); 10945 } 10946 10947 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 10948 }; 10949 10950 mvex = function (tracks) { 10951 var i = tracks.length, 10952 boxes = []; 10953 10954 while (i--) { 10955 boxes[i] = trex(tracks[i]); 10956 } 10957 10958 return box.apply(null, [types.mvex].concat(boxes)); 10959 }; 10960 10961 mvhd = function (duration) { 10962 var bytes = new Uint8Array([0x00, // version 0 10963 0x00, 0x00, 0x00, // flags 10964 0x00, 0x00, 0x00, 0x01, // creation_time 10965 0x00, 0x00, 0x00, 0x02, // modification_time 10966 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 10967 (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration 10968 0x00, 0x01, 0x00, 0x00, // 1.0 rate 10969 0x01, 0x00, // 1.0 volume 10970 0x00, 0x00, // reserved 10971 0x00, 0x00, 0x00, 0x00, // reserved 10972 0x00, 0x00, 0x00, 0x00, // reserved 10973 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 10974 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 10975 0xff, 0xff, 0xff, 0xff // next_track_ID 10976 ]); 10977 return box(types.mvhd, bytes); 10978 }; 10979 10980 sdtp = function (track) { 10981 var samples = track.samples || [], 10982 bytes = new Uint8Array(4 + samples.length), 10983 flags, 10984 i; // leave the full box header (4 bytes) all zero 10985 // write the sample table 10986 10987 for (i = 0; i < samples.length; i++) { 10988 flags = samples[i].flags; 10989 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 10990 } 10991 10992 return box(types.sdtp, bytes); 10993 }; 10994 10995 stbl = function (track) { 10996 return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO)); 10997 }; 10998 10999 (function () { 11000 var videoSample, audioSample; 11001 11002 stsd = function (track) { 11003 return box(types.stsd, new Uint8Array([0x00, // version 0 11004 0x00, 0x00, 0x00, // flags 11005 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track)); 11006 }; 11007 11008 videoSample = function (track) { 11009 var sps = track.sps || [], 11010 pps = track.pps || [], 11011 sequenceParameterSets = [], 11012 pictureParameterSets = [], 11013 i, 11014 avc1Box; // assemble the SPSs 11015 11016 for (i = 0; i < sps.length; i++) { 11017 sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 11018 sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength 11019 11020 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 11021 } // assemble the PPSs 11022 11023 11024 for (i = 0; i < pps.length; i++) { 11025 pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 11026 pictureParameterSets.push(pps[i].byteLength & 0xFF); 11027 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 11028 } 11029 11030 avc1Box = [types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 11031 0x00, 0x01, // data_reference_index 11032 0x00, 0x00, // pre_defined 11033 0x00, 0x00, // reserved 11034 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined 11035 (track.width & 0xff00) >> 8, track.width & 0xff, // width 11036 (track.height & 0xff00) >> 8, track.height & 0xff, // height 11037 0x00, 0x48, 0x00, 0x00, // horizresolution 11038 0x00, 0x48, 0x00, 0x00, // vertresolution 11039 0x00, 0x00, 0x00, 0x00, // reserved 11040 0x00, 0x01, // frame_count 11041 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 11042 0x00, 0x18, // depth = 24 11043 0x11, 0x11 // pre_defined = -1 11044 ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion 11045 track.profileIdc, // AVCProfileIndication 11046 track.profileCompatibility, // profile_compatibility 11047 track.levelIdc, // AVCLevelIndication 11048 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 11049 ].concat([sps.length], // numOfSequenceParameterSets 11050 sequenceParameterSets, // "SPS" 11051 [pps.length], // numOfPictureParameterSets 11052 pictureParameterSets // "PPS" 11053 ))), box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 11054 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 11055 0x00, 0x2d, 0xc6, 0xc0 // avgBitrate 11056 ]))]; 11057 11058 if (track.sarRatio) { 11059 var hSpacing = track.sarRatio[0], 11060 vSpacing = track.sarRatio[1]; 11061 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]))); 11062 } 11063 11064 return box.apply(null, avc1Box); 11065 }; 11066 11067 audioSample = function (track) { 11068 return box(types.mp4a, new Uint8Array([// SampleEntry, ISO/IEC 14496-12 11069 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 11070 0x00, 0x01, // data_reference_index 11071 // AudioSampleEntry, ISO/IEC 14496-12 11072 0x00, 0x00, 0x00, 0x00, // reserved 11073 0x00, 0x00, 0x00, 0x00, // reserved 11074 (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount 11075 (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize 11076 0x00, 0x00, // pre_defined 11077 0x00, 0x00, // reserved 11078 (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16 11079 // MP4AudioSampleEntry, ISO/IEC 14496-14 11080 ]), esds(track)); 11081 }; 11082 })(); 11083 11084 tkhd = function (track) { 11085 var result = new Uint8Array([0x00, // version 0 11086 0x00, 0x00, 0x07, // flags 11087 0x00, 0x00, 0x00, 0x00, // creation_time 11088 0x00, 0x00, 0x00, 0x00, // modification_time 11089 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 11090 0x00, 0x00, 0x00, 0x00, // reserved 11091 (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration 11092 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 11093 0x00, 0x00, // layer 11094 0x00, 0x00, // alternate_group 11095 0x01, 0x00, // non-audio track volume 11096 0x00, 0x00, // reserved 11097 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 11098 (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width 11099 (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height 11100 ]); 11101 return box(types.tkhd, result); 11102 }; 11103 /** 11104 * Generate a track fragment (traf) box. A traf box collects metadata 11105 * about tracks in a movie fragment (moof) box. 11106 */ 11107 11108 11109 traf = function (track) { 11110 var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime; 11111 trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0 11112 0x00, 0x00, 0x3a, // flags 11113 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 11114 0x00, 0x00, 0x00, 0x01, // sample_description_index 11115 0x00, 0x00, 0x00, 0x00, // default_sample_duration 11116 0x00, 0x00, 0x00, 0x00, // default_sample_size 11117 0x00, 0x00, 0x00, 0x00 // default_sample_flags 11118 ])); 11119 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / MAX_UINT32); 11120 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % MAX_UINT32); 11121 trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1 11122 0x00, 0x00, 0x00, // flags 11123 // baseMediaDecodeTime 11124 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 11125 // the containing moof to the first payload byte of the associated 11126 // mdat 11127 11128 dataOffset = 32 + // tfhd 11129 20 + // tfdt 11130 8 + // traf header 11131 16 + // mfhd 11132 8 + // moof header 11133 8; // mdat header 11134 // audio tracks require less metadata 11135 11136 if (track.type === 'audio') { 11137 trackFragmentRun = trun$1(track, dataOffset); 11138 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 11139 } // video tracks should contain an independent and disposable samples 11140 // box (sdtp) 11141 // generate one and adjust offsets to match 11142 11143 11144 sampleDependencyTable = sdtp(track); 11145 trackFragmentRun = trun$1(track, sampleDependencyTable.length + dataOffset); 11146 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable); 11147 }; 11148 /** 11149 * Generate a track box. 11150 * @param track {object} a track definition 11151 * @return {Uint8Array} the track box 11152 */ 11153 11154 11155 trak = function (track) { 11156 track.duration = track.duration || 0xffffffff; 11157 return box(types.trak, tkhd(track), mdia(track)); 11158 }; 11159 11160 trex = function (track) { 11161 var result = new Uint8Array([0x00, // version 0 11162 0x00, 0x00, 0x00, // flags 11163 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 11164 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 11165 0x00, 0x00, 0x00, 0x00, // default_sample_duration 11166 0x00, 0x00, 0x00, 0x00, // default_sample_size 11167 0x00, 0x01, 0x00, 0x01 // default_sample_flags 11168 ]); // the last two bytes of default_sample_flags is the sample 11169 // degradation priority, a hint about the importance of this sample 11170 // relative to others. Lower the degradation priority for all sample 11171 // types other than video. 11172 11173 if (track.type !== 'video') { 11174 result[result.length - 1] = 0x00; 11175 } 11176 11177 return box(types.trex, result); 11178 }; 11179 11180 (function () { 11181 var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a 11182 // duration is present for the first sample, it will be present for 11183 // all subsequent samples. 11184 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 11185 11186 trunHeader = function (samples, offset) { 11187 var durationPresent = 0, 11188 sizePresent = 0, 11189 flagsPresent = 0, 11190 compositionTimeOffset = 0; // trun flag constants 11191 11192 if (samples.length) { 11193 if (samples[0].duration !== undefined) { 11194 durationPresent = 0x1; 11195 } 11196 11197 if (samples[0].size !== undefined) { 11198 sizePresent = 0x2; 11199 } 11200 11201 if (samples[0].flags !== undefined) { 11202 flagsPresent = 0x4; 11203 } 11204 11205 if (samples[0].compositionTimeOffset !== undefined) { 11206 compositionTimeOffset = 0x8; 11207 } 11208 } 11209 11210 return [0x00, // version 0 11211 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags 11212 (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count 11213 (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset 11214 ]; 11215 }; 11216 11217 videoTrun = function (track, offset) { 11218 var bytesOffest, bytes, header, samples, sample, i; 11219 samples = track.samples || []; 11220 offset += 8 + 12 + 16 * samples.length; 11221 header = trunHeader(samples, offset); 11222 bytes = new Uint8Array(header.length + samples.length * 16); 11223 bytes.set(header); 11224 bytesOffest = header.length; 11225 11226 for (i = 0; i < samples.length; i++) { 11227 sample = samples[i]; 11228 bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24; 11229 bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16; 11230 bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8; 11231 bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration 11232 11233 bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24; 11234 bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16; 11235 bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8; 11236 bytes[bytesOffest++] = sample.size & 0xFF; // sample_size 11237 11238 bytes[bytesOffest++] = sample.flags.isLeading << 2 | sample.flags.dependsOn; 11239 bytes[bytesOffest++] = sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample; 11240 bytes[bytesOffest++] = sample.flags.degradationPriority & 0xF0 << 8; 11241 bytes[bytesOffest++] = sample.flags.degradationPriority & 0x0F; // sample_flags 11242 11243 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF000000) >>> 24; 11244 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF0000) >>> 16; 11245 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF00) >>> 8; 11246 bytes[bytesOffest++] = sample.compositionTimeOffset & 0xFF; // sample_composition_time_offset 11247 } 11248 11249 return box(types.trun, bytes); 11250 }; 11251 11252 audioTrun = function (track, offset) { 11253 var bytes, bytesOffest, header, samples, sample, i; 11254 samples = track.samples || []; 11255 offset += 8 + 12 + 8 * samples.length; 11256 header = trunHeader(samples, offset); 11257 bytes = new Uint8Array(header.length + samples.length * 8); 11258 bytes.set(header); 11259 bytesOffest = header.length; 11260 11261 for (i = 0; i < samples.length; i++) { 11262 sample = samples[i]; 11263 bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24; 11264 bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16; 11265 bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8; 11266 bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration 11267 11268 bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24; 11269 bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16; 11270 bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8; 11271 bytes[bytesOffest++] = sample.size & 0xFF; // sample_size 11272 } 11273 11274 return box(types.trun, bytes); 11275 }; 11276 11277 trun$1 = function (track, offset) { 11278 if (track.type === 'audio') { 11279 return audioTrun(track, offset); 11280 } 11281 11282 return videoTrun(track, offset); 11283 }; 11284 })(); 11285 11286 var mp4Generator = { 11287 ftyp: ftyp, 11288 mdat: mdat, 11289 moof: moof, 11290 moov: moov, 11291 initSegment: function (tracks) { 11292 var fileType = ftyp(), 11293 movie = moov(tracks), 11294 result; 11295 result = new Uint8Array(fileType.byteLength + movie.byteLength); 11296 result.set(fileType); 11297 result.set(movie, fileType.byteLength); 11298 return result; 11299 } 11300 }; 11301 /** 11302 * mux.js 11303 * 11304 * Copyright (c) Brightcove 11305 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11306 */ 11307 // composed of the nal units that make up that frame 11308 // Also keep track of cummulative data about the frame from the nal units such 11309 // as the frame duration, starting pts, etc. 11310 11311 var groupNalsIntoFrames = function (nalUnits) { 11312 var i, 11313 currentNal, 11314 currentFrame = [], 11315 frames = []; // TODO added for LHLS, make sure this is OK 11316 11317 frames.byteLength = 0; 11318 frames.nalCount = 0; 11319 frames.duration = 0; 11320 currentFrame.byteLength = 0; 11321 11322 for (i = 0; i < nalUnits.length; i++) { 11323 currentNal = nalUnits[i]; // Split on 'aud'-type nal units 11324 11325 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 11326 // Since the very first nal unit is expected to be an AUD 11327 // only push to the frames array when currentFrame is not empty 11328 if (currentFrame.length) { 11329 currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK 11330 11331 frames.byteLength += currentFrame.byteLength; 11332 frames.nalCount += currentFrame.length; 11333 frames.duration += currentFrame.duration; 11334 frames.push(currentFrame); 11335 } 11336 11337 currentFrame = [currentNal]; 11338 currentFrame.byteLength = currentNal.data.byteLength; 11339 currentFrame.pts = currentNal.pts; 11340 currentFrame.dts = currentNal.dts; 11341 } else { 11342 // Specifically flag key frames for ease of use later 11343 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 11344 currentFrame.keyFrame = true; 11345 } 11346 11347 currentFrame.duration = currentNal.dts - currentFrame.dts; 11348 currentFrame.byteLength += currentNal.data.byteLength; 11349 currentFrame.push(currentNal); 11350 } 11351 } // For the last frame, use the duration of the previous frame if we 11352 // have nothing better to go on 11353 11354 11355 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 11356 currentFrame.duration = frames[frames.length - 1].duration; 11357 } // Push the final frame 11358 // TODO added for LHLS, make sure this is OK 11359 11360 11361 frames.byteLength += currentFrame.byteLength; 11362 frames.nalCount += currentFrame.length; 11363 frames.duration += currentFrame.duration; 11364 frames.push(currentFrame); 11365 return frames; 11366 }; // Convert an array of frames into an array of Gop with each Gop being composed 11367 // of the frames that make up that Gop 11368 // Also keep track of cummulative data about the Gop from the frames such as the 11369 // Gop duration, starting pts, etc. 11370 11371 11372 var groupFramesIntoGops = function (frames) { 11373 var i, 11374 currentFrame, 11375 currentGop = [], 11376 gops = []; // We must pre-set some of the values on the Gop since we 11377 // keep running totals of these values 11378 11379 currentGop.byteLength = 0; 11380 currentGop.nalCount = 0; 11381 currentGop.duration = 0; 11382 currentGop.pts = frames[0].pts; 11383 currentGop.dts = frames[0].dts; // store some metadata about all the Gops 11384 11385 gops.byteLength = 0; 11386 gops.nalCount = 0; 11387 gops.duration = 0; 11388 gops.pts = frames[0].pts; 11389 gops.dts = frames[0].dts; 11390 11391 for (i = 0; i < frames.length; i++) { 11392 currentFrame = frames[i]; 11393 11394 if (currentFrame.keyFrame) { 11395 // Since the very first frame is expected to be an keyframe 11396 // only push to the gops array when currentGop is not empty 11397 if (currentGop.length) { 11398 gops.push(currentGop); 11399 gops.byteLength += currentGop.byteLength; 11400 gops.nalCount += currentGop.nalCount; 11401 gops.duration += currentGop.duration; 11402 } 11403 11404 currentGop = [currentFrame]; 11405 currentGop.nalCount = currentFrame.length; 11406 currentGop.byteLength = currentFrame.byteLength; 11407 currentGop.pts = currentFrame.pts; 11408 currentGop.dts = currentFrame.dts; 11409 currentGop.duration = currentFrame.duration; 11410 } else { 11411 currentGop.duration += currentFrame.duration; 11412 currentGop.nalCount += currentFrame.length; 11413 currentGop.byteLength += currentFrame.byteLength; 11414 currentGop.push(currentFrame); 11415 } 11416 } 11417 11418 if (gops.length && currentGop.duration <= 0) { 11419 currentGop.duration = gops[gops.length - 1].duration; 11420 } 11421 11422 gops.byteLength += currentGop.byteLength; 11423 gops.nalCount += currentGop.nalCount; 11424 gops.duration += currentGop.duration; // push the final Gop 11425 11426 gops.push(currentGop); 11427 return gops; 11428 }; 11429 /* 11430 * Search for the first keyframe in the GOPs and throw away all frames 11431 * until that keyframe. Then extend the duration of the pulled keyframe 11432 * and pull the PTS and DTS of the keyframe so that it covers the time 11433 * range of the frames that were disposed. 11434 * 11435 * @param {Array} gops video GOPs 11436 * @returns {Array} modified video GOPs 11437 */ 11438 11439 11440 var extendFirstKeyFrame = function (gops) { 11441 var currentGop; 11442 11443 if (!gops[0][0].keyFrame && gops.length > 1) { 11444 // Remove the first GOP 11445 currentGop = gops.shift(); 11446 gops.byteLength -= currentGop.byteLength; 11447 gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the 11448 // first gop to cover the time period of the 11449 // frames we just removed 11450 11451 gops[0][0].dts = currentGop.dts; 11452 gops[0][0].pts = currentGop.pts; 11453 gops[0][0].duration += currentGop.duration; 11454 } 11455 11456 return gops; 11457 }; 11458 /** 11459 * Default sample object 11460 * see ISO/IEC 14496-12:2012, section 8.6.4.3 11461 */ 11462 11463 11464 var createDefaultSample = function () { 11465 return { 11466 size: 0, 11467 flags: { 11468 isLeading: 0, 11469 dependsOn: 1, 11470 isDependedOn: 0, 11471 hasRedundancy: 0, 11472 degradationPriority: 0, 11473 isNonSyncSample: 1 11474 } 11475 }; 11476 }; 11477 /* 11478 * Collates information from a video frame into an object for eventual 11479 * entry into an MP4 sample table. 11480 * 11481 * @param {Object} frame the video frame 11482 * @param {Number} dataOffset the byte offset to position the sample 11483 * @return {Object} object containing sample table info for a frame 11484 */ 11485 11486 11487 var sampleForFrame = function (frame, dataOffset) { 11488 var sample = createDefaultSample(); 11489 sample.dataOffset = dataOffset; 11490 sample.compositionTimeOffset = frame.pts - frame.dts; 11491 sample.duration = frame.duration; 11492 sample.size = 4 * frame.length; // Space for nal unit size 11493 11494 sample.size += frame.byteLength; 11495 11496 if (frame.keyFrame) { 11497 sample.flags.dependsOn = 2; 11498 sample.flags.isNonSyncSample = 0; 11499 } 11500 11501 return sample; 11502 }; // generate the track's sample table from an array of gops 11503 11504 11505 var generateSampleTable$1 = function (gops, baseDataOffset) { 11506 var h, 11507 i, 11508 sample, 11509 currentGop, 11510 currentFrame, 11511 dataOffset = baseDataOffset || 0, 11512 samples = []; 11513 11514 for (h = 0; h < gops.length; h++) { 11515 currentGop = gops[h]; 11516 11517 for (i = 0; i < currentGop.length; i++) { 11518 currentFrame = currentGop[i]; 11519 sample = sampleForFrame(currentFrame, dataOffset); 11520 dataOffset += sample.size; 11521 samples.push(sample); 11522 } 11523 } 11524 11525 return samples; 11526 }; // generate the track's raw mdat data from an array of gops 11527 11528 11529 var concatenateNalData = function (gops) { 11530 var h, 11531 i, 11532 j, 11533 currentGop, 11534 currentFrame, 11535 currentNal, 11536 dataOffset = 0, 11537 nalsByteLength = gops.byteLength, 11538 numberOfNals = gops.nalCount, 11539 totalByteLength = nalsByteLength + 4 * numberOfNals, 11540 data = new Uint8Array(totalByteLength), 11541 view = new DataView(data.buffer); // For each Gop.. 11542 11543 for (h = 0; h < gops.length; h++) { 11544 currentGop = gops[h]; // For each Frame.. 11545 11546 for (i = 0; i < currentGop.length; i++) { 11547 currentFrame = currentGop[i]; // For each NAL.. 11548 11549 for (j = 0; j < currentFrame.length; j++) { 11550 currentNal = currentFrame[j]; 11551 view.setUint32(dataOffset, currentNal.data.byteLength); 11552 dataOffset += 4; 11553 data.set(currentNal.data, dataOffset); 11554 dataOffset += currentNal.data.byteLength; 11555 } 11556 } 11557 } 11558 11559 return data; 11560 }; // generate the track's sample table from a frame 11561 11562 11563 var generateSampleTableForFrame = function (frame, baseDataOffset) { 11564 var sample, 11565 dataOffset = baseDataOffset || 0, 11566 samples = []; 11567 sample = sampleForFrame(frame, dataOffset); 11568 samples.push(sample); 11569 return samples; 11570 }; // generate the track's raw mdat data from a frame 11571 11572 11573 var concatenateNalDataForFrame = function (frame) { 11574 var i, 11575 currentNal, 11576 dataOffset = 0, 11577 nalsByteLength = frame.byteLength, 11578 numberOfNals = frame.length, 11579 totalByteLength = nalsByteLength + 4 * numberOfNals, 11580 data = new Uint8Array(totalByteLength), 11581 view = new DataView(data.buffer); // For each NAL.. 11582 11583 for (i = 0; i < frame.length; i++) { 11584 currentNal = frame[i]; 11585 view.setUint32(dataOffset, currentNal.data.byteLength); 11586 dataOffset += 4; 11587 data.set(currentNal.data, dataOffset); 11588 dataOffset += currentNal.data.byteLength; 11589 } 11590 11591 return data; 11592 }; 11593 11594 var frameUtils$1 = { 11595 groupNalsIntoFrames: groupNalsIntoFrames, 11596 groupFramesIntoGops: groupFramesIntoGops, 11597 extendFirstKeyFrame: extendFirstKeyFrame, 11598 generateSampleTable: generateSampleTable$1, 11599 concatenateNalData: concatenateNalData, 11600 generateSampleTableForFrame: generateSampleTableForFrame, 11601 concatenateNalDataForFrame: concatenateNalDataForFrame 11602 }; 11603 /** 11604 * mux.js 11605 * 11606 * Copyright (c) Brightcove 11607 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11608 */ 11609 11610 var highPrefix = [33, 16, 5, 32, 164, 27]; 11611 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 11612 11613 var zeroFill = function (count) { 11614 var a = []; 11615 11616 while (count--) { 11617 a.push(0); 11618 } 11619 11620 return a; 11621 }; 11622 11623 var makeTable = function (metaTable) { 11624 return Object.keys(metaTable).reduce(function (obj, key) { 11625 obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) { 11626 return arr.concat(part); 11627 }, [])); 11628 return obj; 11629 }, {}); 11630 }; 11631 11632 var silence; 11633 11634 var silence_1 = function () { 11635 if (!silence) { 11636 // Frames-of-silence to use for filling in missing AAC frames 11637 var coneOfSilence = { 11638 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 11639 88200: [highPrefix, [231], zeroFill(170), [56]], 11640 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 11641 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 11642 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 11643 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 11644 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]], 11645 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]], 11646 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]], 11647 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]], 11648 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]] 11649 }; 11650 silence = makeTable(coneOfSilence); 11651 } 11652 11653 return silence; 11654 }; 11655 /** 11656 * mux.js 11657 * 11658 * Copyright (c) Brightcove 11659 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11660 */ 11661 11662 11663 var ONE_SECOND_IN_TS$4 = 90000, 11664 // 90kHz clock 11665 secondsToVideoTs, 11666 secondsToAudioTs, 11667 videoTsToSeconds, 11668 audioTsToSeconds, 11669 audioTsToVideoTs, 11670 videoTsToAudioTs, 11671 metadataTsToSeconds; 11672 11673 secondsToVideoTs = function (seconds) { 11674 return seconds * ONE_SECOND_IN_TS$4; 11675 }; 11676 11677 secondsToAudioTs = function (seconds, sampleRate) { 11678 return seconds * sampleRate; 11679 }; 11680 11681 videoTsToSeconds = function (timestamp) { 11682 return timestamp / ONE_SECOND_IN_TS$4; 11683 }; 11684 11685 audioTsToSeconds = function (timestamp, sampleRate) { 11686 return timestamp / sampleRate; 11687 }; 11688 11689 audioTsToVideoTs = function (timestamp, sampleRate) { 11690 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 11691 }; 11692 11693 videoTsToAudioTs = function (timestamp, sampleRate) { 11694 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 11695 }; 11696 /** 11697 * Adjust ID3 tag or caption timing information by the timeline pts values 11698 * (if keepOriginalTimestamps is false) and convert to seconds 11699 */ 11700 11701 11702 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 11703 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 11704 }; 11705 11706 var clock$2 = { 11707 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS$4, 11708 secondsToVideoTs: secondsToVideoTs, 11709 secondsToAudioTs: secondsToAudioTs, 11710 videoTsToSeconds: videoTsToSeconds, 11711 audioTsToSeconds: audioTsToSeconds, 11712 audioTsToVideoTs: audioTsToVideoTs, 11713 videoTsToAudioTs: videoTsToAudioTs, 11714 metadataTsToSeconds: metadataTsToSeconds 11715 }; 11716 /** 11717 * mux.js 11718 * 11719 * Copyright (c) Brightcove 11720 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11721 */ 11722 11723 var coneOfSilence = silence_1; 11724 var clock$1 = clock$2; 11725 /** 11726 * Sum the `byteLength` properties of the data in each AAC frame 11727 */ 11728 11729 var sumFrameByteLengths = function (array) { 11730 var i, 11731 currentObj, 11732 sum = 0; // sum the byteLength's all each nal unit in the frame 11733 11734 for (i = 0; i < array.length; i++) { 11735 currentObj = array[i]; 11736 sum += currentObj.data.byteLength; 11737 } 11738 11739 return sum; 11740 }; // Possibly pad (prefix) the audio track with silence if appending this track 11741 // would lead to the introduction of a gap in the audio buffer 11742 11743 11744 var prefixWithSilence = function (track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) { 11745 var baseMediaDecodeTimeTs, 11746 frameDuration = 0, 11747 audioGapDuration = 0, 11748 audioFillFrameCount = 0, 11749 audioFillDuration = 0, 11750 silentFrame, 11751 i, 11752 firstFrame; 11753 11754 if (!frames.length) { 11755 return; 11756 } 11757 11758 baseMediaDecodeTimeTs = clock$1.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills 11759 11760 frameDuration = Math.ceil(clock$1.ONE_SECOND_IN_TS / (track.samplerate / 1024)); 11761 11762 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 11763 // insert the shortest possible amount (audio gap or audio to video gap) 11764 audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap 11765 11766 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 11767 audioFillDuration = audioFillFrameCount * frameDuration; 11768 } // don't attempt to fill gaps smaller than a single frame or larger 11769 // than a half second 11770 11771 11772 if (audioFillFrameCount < 1 || audioFillDuration > clock$1.ONE_SECOND_IN_TS / 2) { 11773 return; 11774 } 11775 11776 silentFrame = coneOfSilence()[track.samplerate]; 11777 11778 if (!silentFrame) { 11779 // we don't have a silent frame pregenerated for the sample rate, so use a frame 11780 // from the content instead 11781 silentFrame = frames[0].data; 11782 } 11783 11784 for (i = 0; i < audioFillFrameCount; i++) { 11785 firstFrame = frames[0]; 11786 frames.splice(0, 0, { 11787 data: silentFrame, 11788 dts: firstFrame.dts - frameDuration, 11789 pts: firstFrame.pts - frameDuration 11790 }); 11791 } 11792 11793 track.baseMediaDecodeTime -= Math.floor(clock$1.videoTsToAudioTs(audioFillDuration, track.samplerate)); 11794 return audioFillDuration; 11795 }; // If the audio segment extends before the earliest allowed dts 11796 // value, remove AAC frames until starts at or after the earliest 11797 // allowed DTS so that we don't end up with a negative baseMedia- 11798 // DecodeTime for the audio track 11799 11800 11801 var trimAdtsFramesByEarliestDts = function (adtsFrames, track, earliestAllowedDts) { 11802 if (track.minSegmentDts >= earliestAllowedDts) { 11803 return adtsFrames; 11804 } // We will need to recalculate the earliest segment Dts 11805 11806 11807 track.minSegmentDts = Infinity; 11808 return adtsFrames.filter(function (currentFrame) { 11809 // If this is an allowed frame, keep it and record it's Dts 11810 if (currentFrame.dts >= earliestAllowedDts) { 11811 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 11812 track.minSegmentPts = track.minSegmentDts; 11813 return true; 11814 } // Otherwise, discard it 11815 11816 11817 return false; 11818 }); 11819 }; // generate the track's raw mdat data from an array of frames 11820 11821 11822 var generateSampleTable = function (frames) { 11823 var i, 11824 currentFrame, 11825 samples = []; 11826 11827 for (i = 0; i < frames.length; i++) { 11828 currentFrame = frames[i]; 11829 samples.push({ 11830 size: currentFrame.data.byteLength, 11831 duration: 1024 // For AAC audio, all samples contain 1024 samples 11832 11833 }); 11834 } 11835 11836 return samples; 11837 }; // generate the track's sample table from an array of frames 11838 11839 11840 var concatenateFrameData = function (frames) { 11841 var i, 11842 currentFrame, 11843 dataOffset = 0, 11844 data = new Uint8Array(sumFrameByteLengths(frames)); 11845 11846 for (i = 0; i < frames.length; i++) { 11847 currentFrame = frames[i]; 11848 data.set(currentFrame.data, dataOffset); 11849 dataOffset += currentFrame.data.byteLength; 11850 } 11851 11852 return data; 11853 }; 11854 11855 var audioFrameUtils$1 = { 11856 prefixWithSilence: prefixWithSilence, 11857 trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts, 11858 generateSampleTable: generateSampleTable, 11859 concatenateFrameData: concatenateFrameData 11860 }; 11861 /** 11862 * mux.js 11863 * 11864 * Copyright (c) Brightcove 11865 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11866 */ 11867 11868 var ONE_SECOND_IN_TS$3 = clock$2.ONE_SECOND_IN_TS; 11869 /** 11870 * Store information about the start and end of the track and the 11871 * duration for each frame/sample we process in order to calculate 11872 * the baseMediaDecodeTime 11873 */ 11874 11875 var collectDtsInfo = function (track, data) { 11876 if (typeof data.pts === 'number') { 11877 if (track.timelineStartInfo.pts === undefined) { 11878 track.timelineStartInfo.pts = data.pts; 11879 } 11880 11881 if (track.minSegmentPts === undefined) { 11882 track.minSegmentPts = data.pts; 11883 } else { 11884 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 11885 } 11886 11887 if (track.maxSegmentPts === undefined) { 11888 track.maxSegmentPts = data.pts; 11889 } else { 11890 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 11891 } 11892 } 11893 11894 if (typeof data.dts === 'number') { 11895 if (track.timelineStartInfo.dts === undefined) { 11896 track.timelineStartInfo.dts = data.dts; 11897 } 11898 11899 if (track.minSegmentDts === undefined) { 11900 track.minSegmentDts = data.dts; 11901 } else { 11902 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 11903 } 11904 11905 if (track.maxSegmentDts === undefined) { 11906 track.maxSegmentDts = data.dts; 11907 } else { 11908 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 11909 } 11910 } 11911 }; 11912 /** 11913 * Clear values used to calculate the baseMediaDecodeTime between 11914 * tracks 11915 */ 11916 11917 11918 var clearDtsInfo = function (track) { 11919 delete track.minSegmentDts; 11920 delete track.maxSegmentDts; 11921 delete track.minSegmentPts; 11922 delete track.maxSegmentPts; 11923 }; 11924 /** 11925 * Calculate the track's baseMediaDecodeTime based on the earliest 11926 * DTS the transmuxer has ever seen and the minimum DTS for the 11927 * current track 11928 * @param track {object} track metadata configuration 11929 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 11930 * in the source; false to adjust the first segment to start at 0. 11931 */ 11932 11933 11934 var calculateTrackBaseMediaDecodeTime = function (track, keepOriginalTimestamps) { 11935 var baseMediaDecodeTime, 11936 scale, 11937 minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero. 11938 11939 if (!keepOriginalTimestamps) { 11940 minSegmentDts -= track.timelineStartInfo.dts; 11941 } // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 11942 // we want the start of the first segment to be placed 11943 11944 11945 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first 11946 11947 baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative 11948 11949 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 11950 11951 if (track.type === 'audio') { 11952 // Audio has a different clock equal to the sampling_rate so we need to 11953 // scale the PTS values into the clock rate of the track 11954 scale = track.samplerate / ONE_SECOND_IN_TS$3; 11955 baseMediaDecodeTime *= scale; 11956 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 11957 } 11958 11959 return baseMediaDecodeTime; 11960 }; 11961 11962 var trackDecodeInfo$1 = { 11963 clearDtsInfo: clearDtsInfo, 11964 calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime, 11965 collectDtsInfo: collectDtsInfo 11966 }; 11967 /** 11968 * mux.js 11969 * 11970 * Copyright (c) Brightcove 11971 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 11972 * 11973 * Reads in-band caption information from a video elementary 11974 * stream. Captions must follow the CEA-708 standard for injection 11975 * into an MPEG-2 transport streams. 11976 * @see https://en.wikipedia.org/wiki/CEA-708 11977 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 11978 */ 11979 // payload type field to indicate how they are to be 11980 // interpreted. CEAS-708 caption content is always transmitted with 11981 // payload type 0x04. 11982 11983 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 11984 RBSP_TRAILING_BITS = 128; 11985 /** 11986 * Parse a supplemental enhancement information (SEI) NAL unit. 11987 * Stops parsing once a message of type ITU T T35 has been found. 11988 * 11989 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 11990 * @return {object} the parsed SEI payload 11991 * @see Rec. ITU-T H.264, 7.3.2.3.1 11992 */ 11993 11994 var parseSei = function (bytes) { 11995 var i = 0, 11996 result = { 11997 payloadType: -1, 11998 payloadSize: 0 11999 }, 12000 payloadType = 0, 12001 payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message 12002 12003 while (i < bytes.byteLength) { 12004 // stop once we have hit the end of the sei_rbsp 12005 if (bytes[i] === RBSP_TRAILING_BITS) { 12006 break; 12007 } // Parse payload type 12008 12009 12010 while (bytes[i] === 0xFF) { 12011 payloadType += 255; 12012 i++; 12013 } 12014 12015 payloadType += bytes[i++]; // Parse payload size 12016 12017 while (bytes[i] === 0xFF) { 12018 payloadSize += 255; 12019 i++; 12020 } 12021 12022 payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break 12023 // there can only ever be one caption message in a frame's sei 12024 12025 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 12026 var userIdentifier = String.fromCharCode(bytes[i + 3], bytes[i + 4], bytes[i + 5], bytes[i + 6]); 12027 12028 if (userIdentifier === 'GA94') { 12029 result.payloadType = payloadType; 12030 result.payloadSize = payloadSize; 12031 result.payload = bytes.subarray(i, i + payloadSize); 12032 break; 12033 } else { 12034 result.payload = void 0; 12035 } 12036 } // skip the payload and parse the next message 12037 12038 12039 i += payloadSize; 12040 payloadType = 0; 12041 payloadSize = 0; 12042 } 12043 12044 return result; 12045 }; // see ANSI/SCTE 128-1 (2013), section 8.1 12046 12047 12048 var parseUserData = function (sei) { 12049 // itu_t_t35_contry_code must be 181 (United States) for 12050 // captions 12051 if (sei.payload[0] !== 181) { 12052 return null; 12053 } // itu_t_t35_provider_code should be 49 (ATSC) for captions 12054 12055 12056 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 12057 return null; 12058 } // the user_identifier should be "GA94" to indicate ATSC1 data 12059 12060 12061 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 12062 return null; 12063 } // finally, user_data_type_code should be 0x03 for caption data 12064 12065 12066 if (sei.payload[7] !== 0x03) { 12067 return null; 12068 } // return the user_data_type_structure and strip the trailing 12069 // marker bits 12070 12071 12072 return sei.payload.subarray(8, sei.payload.length - 1); 12073 }; // see CEA-708-D, section 4.4 12074 12075 12076 var parseCaptionPackets = function (pts, userData) { 12077 var results = [], 12078 i, 12079 count, 12080 offset, 12081 data; // if this is just filler, return immediately 12082 12083 if (!(userData[0] & 0x40)) { 12084 return results; 12085 } // parse out the cc_data_1 and cc_data_2 fields 12086 12087 12088 count = userData[0] & 0x1f; 12089 12090 for (i = 0; i < count; i++) { 12091 offset = i * 3; 12092 data = { 12093 type: userData[offset + 2] & 0x03, 12094 pts: pts 12095 }; // capture cc data when cc_valid is 1 12096 12097 if (userData[offset + 2] & 0x04) { 12098 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 12099 results.push(data); 12100 } 12101 } 12102 12103 return results; 12104 }; 12105 12106 var discardEmulationPreventionBytes$1 = function (data) { 12107 var length = data.byteLength, 12108 emulationPreventionBytesPositions = [], 12109 i = 1, 12110 newLength, 12111 newData; // Find all `Emulation Prevention Bytes` 12112 12113 while (i < length - 2) { 12114 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 12115 emulationPreventionBytesPositions.push(i + 2); 12116 i += 2; 12117 } else { 12118 i++; 12119 } 12120 } // If no Emulation Prevention Bytes were found just return the original 12121 // array 12122 12123 12124 if (emulationPreventionBytesPositions.length === 0) { 12125 return data; 12126 } // Create a new array to hold the NAL unit data 12127 12128 12129 newLength = length - emulationPreventionBytesPositions.length; 12130 newData = new Uint8Array(newLength); 12131 var sourceIndex = 0; 12132 12133 for (i = 0; i < newLength; sourceIndex++, i++) { 12134 if (sourceIndex === emulationPreventionBytesPositions[0]) { 12135 // Skip this byte 12136 sourceIndex++; // Remove this position index 12137 12138 emulationPreventionBytesPositions.shift(); 12139 } 12140 12141 newData[i] = data[sourceIndex]; 12142 } 12143 12144 return newData; 12145 }; // exports 12146 12147 12148 var captionPacketParser = { 12149 parseSei: parseSei, 12150 parseUserData: parseUserData, 12151 parseCaptionPackets: parseCaptionPackets, 12152 discardEmulationPreventionBytes: discardEmulationPreventionBytes$1, 12153 USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35 12154 }; 12155 /** 12156 * mux.js 12157 * 12158 * Copyright (c) Brightcove 12159 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 12160 * 12161 * Reads in-band caption information from a video elementary 12162 * stream. Captions must follow the CEA-708 standard for injection 12163 * into an MPEG-2 transport streams. 12164 * @see https://en.wikipedia.org/wiki/CEA-708 12165 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 12166 */ 12167 // Link To Transport 12168 // ----------------- 12169 12170 var Stream$7 = stream; 12171 var cea708Parser = captionPacketParser; 12172 12173 var CaptionStream$2 = function (options) { 12174 options = options || {}; 12175 CaptionStream$2.prototype.init.call(this); // parse708captions flag, default to true 12176 12177 this.parse708captions_ = typeof options.parse708captions === 'boolean' ? options.parse708captions : true; 12178 this.captionPackets_ = []; 12179 this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define 12180 new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define 12181 new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define 12182 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 12183 ]; 12184 12185 if (this.parse708captions_) { 12186 this.cc708Stream_ = new Cea708Stream({ 12187 captionServices: options.captionServices 12188 }); // eslint-disable-line no-use-before-define 12189 } 12190 12191 this.reset(); // forward data and done events from CCs to this CaptionStream 12192 12193 this.ccStreams_.forEach(function (cc) { 12194 cc.on('data', this.trigger.bind(this, 'data')); 12195 cc.on('partialdone', this.trigger.bind(this, 'partialdone')); 12196 cc.on('done', this.trigger.bind(this, 'done')); 12197 }, this); 12198 12199 if (this.parse708captions_) { 12200 this.cc708Stream_.on('data', this.trigger.bind(this, 'data')); 12201 this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone')); 12202 this.cc708Stream_.on('done', this.trigger.bind(this, 'done')); 12203 } 12204 }; 12205 12206 CaptionStream$2.prototype = new Stream$7(); 12207 12208 CaptionStream$2.prototype.push = function (event) { 12209 var sei, userData, newCaptionPackets; // only examine SEI NALs 12210 12211 if (event.nalUnitType !== 'sei_rbsp') { 12212 return; 12213 } // parse the sei 12214 12215 12216 sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip 12217 12218 if (!sei.payload) { 12219 return; 12220 } // ignore everything but user_data_registered_itu_t_t35 12221 12222 12223 if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) { 12224 return; 12225 } // parse out the user data payload 12226 12227 12228 userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData 12229 12230 if (!userData) { 12231 return; 12232 } // Sometimes, the same segment # will be downloaded twice. To stop the 12233 // caption data from being processed twice, we track the latest dts we've 12234 // received and ignore everything with a dts before that. However, since 12235 // data for a specific dts can be split across packets on either side of 12236 // a segment boundary, we need to make sure we *don't* ignore the packets 12237 // from the *next* segment that have dts === this.latestDts_. By constantly 12238 // tracking the number of packets received with dts === this.latestDts_, we 12239 // know how many should be ignored once we start receiving duplicates. 12240 12241 12242 if (event.dts < this.latestDts_) { 12243 // We've started getting older data, so set the flag. 12244 this.ignoreNextEqualDts_ = true; 12245 return; 12246 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 12247 this.numSameDts_--; 12248 12249 if (!this.numSameDts_) { 12250 // We've received the last duplicate packet, time to start processing again 12251 this.ignoreNextEqualDts_ = false; 12252 } 12253 12254 return; 12255 } // parse out CC data packets and save them for later 12256 12257 12258 newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData); 12259 this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); 12260 12261 if (this.latestDts_ !== event.dts) { 12262 this.numSameDts_ = 0; 12263 } 12264 12265 this.numSameDts_++; 12266 this.latestDts_ = event.dts; 12267 }; 12268 12269 CaptionStream$2.prototype.flushCCStreams = function (flushType) { 12270 this.ccStreams_.forEach(function (cc) { 12271 return flushType === 'flush' ? cc.flush() : cc.partialFlush(); 12272 }, this); 12273 }; 12274 12275 CaptionStream$2.prototype.flushStream = function (flushType) { 12276 // make sure we actually parsed captions before proceeding 12277 if (!this.captionPackets_.length) { 12278 this.flushCCStreams(flushType); 12279 return; 12280 } // In Chrome, the Array#sort function is not stable so add a 12281 // presortIndex that we can use to ensure we get a stable-sort 12282 12283 12284 this.captionPackets_.forEach(function (elem, idx) { 12285 elem.presortIndex = idx; 12286 }); // sort caption byte-pairs based on their PTS values 12287 12288 this.captionPackets_.sort(function (a, b) { 12289 if (a.pts === b.pts) { 12290 return a.presortIndex - b.presortIndex; 12291 } 12292 12293 return a.pts - b.pts; 12294 }); 12295 this.captionPackets_.forEach(function (packet) { 12296 if (packet.type < 2) { 12297 // Dispatch packet to the right Cea608Stream 12298 this.dispatchCea608Packet(packet); 12299 } else { 12300 // Dispatch packet to the Cea708Stream 12301 this.dispatchCea708Packet(packet); 12302 } 12303 }, this); 12304 this.captionPackets_.length = 0; 12305 this.flushCCStreams(flushType); 12306 }; 12307 12308 CaptionStream$2.prototype.flush = function () { 12309 return this.flushStream('flush'); 12310 }; // Only called if handling partial data 12311 12312 12313 CaptionStream$2.prototype.partialFlush = function () { 12314 return this.flushStream('partialFlush'); 12315 }; 12316 12317 CaptionStream$2.prototype.reset = function () { 12318 this.latestDts_ = null; 12319 this.ignoreNextEqualDts_ = false; 12320 this.numSameDts_ = 0; 12321 this.activeCea608Channel_ = [null, null]; 12322 this.ccStreams_.forEach(function (ccStream) { 12323 ccStream.reset(); 12324 }); 12325 }; // From the CEA-608 spec: 12326 12327 /* 12328 * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed 12329 * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is 12330 * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair 12331 * and subsequent data should then be processed according to the FCC rules. It may be necessary for the 12332 * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) 12333 * to switch to captioning or Text. 12334 */ 12335 // With that in mind, we ignore any data between an XDS control code and a 12336 // subsequent closed-captioning control code. 12337 12338 12339 CaptionStream$2.prototype.dispatchCea608Packet = function (packet) { 12340 // NOTE: packet.type is the CEA608 field 12341 if (this.setsTextOrXDSActive(packet)) { 12342 this.activeCea608Channel_[packet.type] = null; 12343 } else if (this.setsChannel1Active(packet)) { 12344 this.activeCea608Channel_[packet.type] = 0; 12345 } else if (this.setsChannel2Active(packet)) { 12346 this.activeCea608Channel_[packet.type] = 1; 12347 } 12348 12349 if (this.activeCea608Channel_[packet.type] === null) { 12350 // If we haven't received anything to set the active channel, or the 12351 // packets are Text/XDS data, discard the data; we don't want jumbled 12352 // captions 12353 return; 12354 } 12355 12356 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 12357 }; 12358 12359 CaptionStream$2.prototype.setsChannel1Active = function (packet) { 12360 return (packet.ccData & 0x7800) === 0x1000; 12361 }; 12362 12363 CaptionStream$2.prototype.setsChannel2Active = function (packet) { 12364 return (packet.ccData & 0x7800) === 0x1800; 12365 }; 12366 12367 CaptionStream$2.prototype.setsTextOrXDSActive = function (packet) { 12368 return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a; 12369 }; 12370 12371 CaptionStream$2.prototype.dispatchCea708Packet = function (packet) { 12372 if (this.parse708captions_) { 12373 this.cc708Stream_.push(packet); 12374 } 12375 }; // ---------------------- 12376 // Session to Application 12377 // ---------------------- 12378 // This hash maps special and extended character codes to their 12379 // proper Unicode equivalent. The first one-byte key is just a 12380 // non-standard character code. The two-byte keys that follow are 12381 // the extended CEA708 character codes, along with the preceding 12382 // 0x10 extended character byte to distinguish these codes from 12383 // non-extended character codes. Every CEA708 character code that 12384 // is not in this object maps directly to a standard unicode 12385 // character code. 12386 // The transparent space and non-breaking transparent space are 12387 // technically not fully supported since there is no code to 12388 // make them transparent, so they have normal non-transparent 12389 // stand-ins. 12390 // The special closed caption (CC) character isn't a standard 12391 // unicode character, so a fairly similar unicode character was 12392 // chosen in it's place. 12393 12394 12395 var CHARACTER_TRANSLATION_708 = { 12396 0x7f: 0x266a, 12397 // ⪠12398 0x1020: 0x20, 12399 // Transparent Space 12400 0x1021: 0xa0, 12401 // Nob-breaking Transparent Space 12402 0x1025: 0x2026, 12403 // ⦠12404 0x102a: 0x0160, 12405 // Å 12406 0x102c: 0x0152, 12407 // Å 12408 0x1030: 0x2588, 12409 // â 12410 0x1031: 0x2018, 12411 // â 12412 0x1032: 0x2019, 12413 // â 12414 0x1033: 0x201c, 12415 // â 12416 0x1034: 0x201d, 12417 // â 12418 0x1035: 0x2022, 12419 // ⢠12420 0x1039: 0x2122, 12421 // ⢠12422 0x103a: 0x0161, 12423 // Å¡ 12424 0x103c: 0x0153, 12425 // Å 12426 0x103d: 0x2120, 12427 // â 12428 0x103f: 0x0178, 12429 // Ÿ 12430 0x1076: 0x215b, 12431 // â 12432 0x1077: 0x215c, 12433 // â 12434 0x1078: 0x215d, 12435 // â 12436 0x1079: 0x215e, 12437 // â 12438 0x107a: 0x23d0, 12439 // â 12440 0x107b: 0x23a4, 12441 // ⤠12442 0x107c: 0x23a3, 12443 // ⣠12444 0x107d: 0x23af, 12445 // ⯠12446 0x107e: 0x23a6, 12447 // ⦠12448 0x107f: 0x23a1, 12449 // â¡ 12450 0x10a0: 0x3138 // 㸠(CC char) 12451 12452 }; 12453 12454 var get708CharFromCode = function (code) { 12455 var newCode = CHARACTER_TRANSLATION_708[code] || code; 12456 12457 if (code & 0x1000 && code === newCode) { 12458 // Invalid extended code 12459 return ''; 12460 } 12461 12462 return String.fromCharCode(newCode); 12463 }; 12464 12465 var within708TextBlock = function (b) { 12466 return 0x20 <= b && b <= 0x7f || 0xa0 <= b && b <= 0xff; 12467 }; 12468 12469 var Cea708Window = function (windowNum) { 12470 this.windowNum = windowNum; 12471 this.reset(); 12472 }; 12473 12474 Cea708Window.prototype.reset = function () { 12475 this.clearText(); 12476 this.pendingNewLine = false; 12477 this.winAttr = {}; 12478 this.penAttr = {}; 12479 this.penLoc = {}; 12480 this.penColor = {}; // These default values are arbitrary, 12481 // defineWindow will usually override them 12482 12483 this.visible = 0; 12484 this.rowLock = 0; 12485 this.columnLock = 0; 12486 this.priority = 0; 12487 this.relativePositioning = 0; 12488 this.anchorVertical = 0; 12489 this.anchorHorizontal = 0; 12490 this.anchorPoint = 0; 12491 this.rowCount = 1; 12492 this.virtualRowCount = this.rowCount + 1; 12493 this.columnCount = 41; 12494 this.windowStyle = 0; 12495 this.penStyle = 0; 12496 }; 12497 12498 Cea708Window.prototype.getText = function () { 12499 return this.rows.join('\n'); 12500 }; 12501 12502 Cea708Window.prototype.clearText = function () { 12503 this.rows = ['']; 12504 this.rowIdx = 0; 12505 }; 12506 12507 Cea708Window.prototype.newLine = function (pts) { 12508 if (this.rows.length >= this.virtualRowCount && typeof this.beforeRowOverflow === 'function') { 12509 this.beforeRowOverflow(pts); 12510 } 12511 12512 if (this.rows.length > 0) { 12513 this.rows.push(''); 12514 this.rowIdx++; 12515 } // Show all virtual rows since there's no visible scrolling 12516 12517 12518 while (this.rows.length > this.virtualRowCount) { 12519 this.rows.shift(); 12520 this.rowIdx--; 12521 } 12522 }; 12523 12524 Cea708Window.prototype.isEmpty = function () { 12525 if (this.rows.length === 0) { 12526 return true; 12527 } else if (this.rows.length === 1) { 12528 return this.rows[0] === ''; 12529 } 12530 12531 return false; 12532 }; 12533 12534 Cea708Window.prototype.addText = function (text) { 12535 this.rows[this.rowIdx] += text; 12536 }; 12537 12538 Cea708Window.prototype.backspace = function () { 12539 if (!this.isEmpty()) { 12540 var row = this.rows[this.rowIdx]; 12541 this.rows[this.rowIdx] = row.substr(0, row.length - 1); 12542 } 12543 }; 12544 12545 var Cea708Service = function (serviceNum, encoding, stream) { 12546 this.serviceNum = serviceNum; 12547 this.text = ''; 12548 this.currentWindow = new Cea708Window(-1); 12549 this.windows = []; 12550 this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided 12551 12552 if (typeof encoding === 'string') { 12553 this.createTextDecoder(encoding); 12554 } 12555 }; 12556 /** 12557 * Initialize service windows 12558 * Must be run before service use 12559 * 12560 * @param {Integer} pts PTS value 12561 * @param {Function} beforeRowOverflow Function to execute before row overflow of a window 12562 */ 12563 12564 12565 Cea708Service.prototype.init = function (pts, beforeRowOverflow) { 12566 this.startPts = pts; 12567 12568 for (var win = 0; win < 8; win++) { 12569 this.windows[win] = new Cea708Window(win); 12570 12571 if (typeof beforeRowOverflow === 'function') { 12572 this.windows[win].beforeRowOverflow = beforeRowOverflow; 12573 } 12574 } 12575 }; 12576 /** 12577 * Set current window of service to be affected by commands 12578 * 12579 * @param {Integer} windowNum Window number 12580 */ 12581 12582 12583 Cea708Service.prototype.setCurrentWindow = function (windowNum) { 12584 this.currentWindow = this.windows[windowNum]; 12585 }; 12586 /** 12587 * Try to create a TextDecoder if it is natively supported 12588 */ 12589 12590 12591 Cea708Service.prototype.createTextDecoder = function (encoding) { 12592 if (typeof TextDecoder === 'undefined') { 12593 this.stream.trigger('log', { 12594 level: 'warn', 12595 message: 'The `encoding` option is unsupported without TextDecoder support' 12596 }); 12597 } else { 12598 try { 12599 this.textDecoder_ = new TextDecoder(encoding); 12600 } catch (error) { 12601 this.stream.trigger('log', { 12602 level: 'warn', 12603 message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error 12604 }); 12605 } 12606 } 12607 }; 12608 12609 var Cea708Stream = function (options) { 12610 options = options || {}; 12611 Cea708Stream.prototype.init.call(this); 12612 var self = this; 12613 var captionServices = options.captionServices || {}; 12614 var captionServiceEncodings = {}; 12615 var serviceProps; // Get service encodings from captionServices option block 12616 12617 Object.keys(captionServices).forEach(serviceName => { 12618 serviceProps = captionServices[serviceName]; 12619 12620 if (/^SERVICE/.test(serviceName)) { 12621 captionServiceEncodings[serviceName] = serviceProps.encoding; 12622 } 12623 }); 12624 this.serviceEncodings = captionServiceEncodings; 12625 this.current708Packet = null; 12626 this.services = {}; 12627 12628 this.push = function (packet) { 12629 if (packet.type === 3) { 12630 // 708 packet start 12631 self.new708Packet(); 12632 self.add708Bytes(packet); 12633 } else { 12634 if (self.current708Packet === null) { 12635 // This should only happen at the start of a file if there's no packet start. 12636 self.new708Packet(); 12637 } 12638 12639 self.add708Bytes(packet); 12640 } 12641 }; 12642 }; 12643 12644 Cea708Stream.prototype = new Stream$7(); 12645 /** 12646 * Push current 708 packet, create new 708 packet. 12647 */ 12648 12649 Cea708Stream.prototype.new708Packet = function () { 12650 if (this.current708Packet !== null) { 12651 this.push708Packet(); 12652 } 12653 12654 this.current708Packet = { 12655 data: [], 12656 ptsVals: [] 12657 }; 12658 }; 12659 /** 12660 * Add pts and both bytes from packet into current 708 packet. 12661 */ 12662 12663 12664 Cea708Stream.prototype.add708Bytes = function (packet) { 12665 var data = packet.ccData; 12666 var byte0 = data >>> 8; 12667 var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec 12668 // that service blocks will always line up with byte pairs. 12669 12670 this.current708Packet.ptsVals.push(packet.pts); 12671 this.current708Packet.data.push(byte0); 12672 this.current708Packet.data.push(byte1); 12673 }; 12674 /** 12675 * Parse completed 708 packet into service blocks and push each service block. 12676 */ 12677 12678 12679 Cea708Stream.prototype.push708Packet = function () { 12680 var packet708 = this.current708Packet; 12681 var packetData = packet708.data; 12682 var serviceNum = null; 12683 var blockSize = null; 12684 var i = 0; 12685 var b = packetData[i++]; 12686 packet708.seq = b >> 6; 12687 packet708.sizeCode = b & 0x3f; // 0b00111111; 12688 12689 for (; i < packetData.length; i++) { 12690 b = packetData[i++]; 12691 serviceNum = b >> 5; 12692 blockSize = b & 0x1f; // 0b00011111 12693 12694 if (serviceNum === 7 && blockSize > 0) { 12695 // Extended service num 12696 b = packetData[i++]; 12697 serviceNum = b; 12698 } 12699 12700 this.pushServiceBlock(serviceNum, i, blockSize); 12701 12702 if (blockSize > 0) { 12703 i += blockSize - 1; 12704 } 12705 } 12706 }; 12707 /** 12708 * Parse service block, execute commands, read text. 12709 * 12710 * Note: While many of these commands serve important purposes, 12711 * many others just parse out the parameters or attributes, but 12712 * nothing is done with them because this is not a full and complete 12713 * implementation of the entire 708 spec. 12714 * 12715 * @param {Integer} serviceNum Service number 12716 * @param {Integer} start Start index of the 708 packet data 12717 * @param {Integer} size Block size 12718 */ 12719 12720 12721 Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) { 12722 var b; 12723 var i = start; 12724 var packetData = this.current708Packet.data; 12725 var service = this.services[serviceNum]; 12726 12727 if (!service) { 12728 service = this.initService(serviceNum, i); 12729 } 12730 12731 for (; i < start + size && i < packetData.length; i++) { 12732 b = packetData[i]; 12733 12734 if (within708TextBlock(b)) { 12735 i = this.handleText(i, service); 12736 } else if (b === 0x18) { 12737 i = this.multiByteCharacter(i, service); 12738 } else if (b === 0x10) { 12739 i = this.extendedCommands(i, service); 12740 } else if (0x80 <= b && b <= 0x87) { 12741 i = this.setCurrentWindow(i, service); 12742 } else if (0x98 <= b && b <= 0x9f) { 12743 i = this.defineWindow(i, service); 12744 } else if (b === 0x88) { 12745 i = this.clearWindows(i, service); 12746 } else if (b === 0x8c) { 12747 i = this.deleteWindows(i, service); 12748 } else if (b === 0x89) { 12749 i = this.displayWindows(i, service); 12750 } else if (b === 0x8a) { 12751 i = this.hideWindows(i, service); 12752 } else if (b === 0x8b) { 12753 i = this.toggleWindows(i, service); 12754 } else if (b === 0x97) { 12755 i = this.setWindowAttributes(i, service); 12756 } else if (b === 0x90) { 12757 i = this.setPenAttributes(i, service); 12758 } else if (b === 0x91) { 12759 i = this.setPenColor(i, service); 12760 } else if (b === 0x92) { 12761 i = this.setPenLocation(i, service); 12762 } else if (b === 0x8f) { 12763 service = this.reset(i, service); 12764 } else if (b === 0x08) { 12765 // BS: Backspace 12766 service.currentWindow.backspace(); 12767 } else if (b === 0x0c) { 12768 // FF: Form feed 12769 service.currentWindow.clearText(); 12770 } else if (b === 0x0d) { 12771 // CR: Carriage return 12772 service.currentWindow.pendingNewLine = true; 12773 } else if (b === 0x0e) { 12774 // HCR: Horizontal carriage return 12775 service.currentWindow.clearText(); 12776 } else if (b === 0x8d) { 12777 // DLY: Delay, nothing to do 12778 i++; 12779 } else ; 12780 } 12781 }; 12782 /** 12783 * Execute an extended command 12784 * 12785 * @param {Integer} i Current index in the 708 packet 12786 * @param {Service} service The service object to be affected 12787 * @return {Integer} New index after parsing 12788 */ 12789 12790 12791 Cea708Stream.prototype.extendedCommands = function (i, service) { 12792 var packetData = this.current708Packet.data; 12793 var b = packetData[++i]; 12794 12795 if (within708TextBlock(b)) { 12796 i = this.handleText(i, service, { 12797 isExtended: true 12798 }); 12799 } 12800 12801 return i; 12802 }; 12803 /** 12804 * Get PTS value of a given byte index 12805 * 12806 * @param {Integer} byteIndex Index of the byte 12807 * @return {Integer} PTS 12808 */ 12809 12810 12811 Cea708Stream.prototype.getPts = function (byteIndex) { 12812 // There's 1 pts value per 2 bytes 12813 return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)]; 12814 }; 12815 /** 12816 * Initializes a service 12817 * 12818 * @param {Integer} serviceNum Service number 12819 * @return {Service} Initialized service object 12820 */ 12821 12822 12823 Cea708Stream.prototype.initService = function (serviceNum, i) { 12824 var serviceName = 'SERVICE' + serviceNum; 12825 var self = this; 12826 var serviceName; 12827 var encoding; 12828 12829 if (serviceName in this.serviceEncodings) { 12830 encoding = this.serviceEncodings[serviceName]; 12831 } 12832 12833 this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self); 12834 this.services[serviceNum].init(this.getPts(i), function (pts) { 12835 self.flushDisplayed(pts, self.services[serviceNum]); 12836 }); 12837 return this.services[serviceNum]; 12838 }; 12839 /** 12840 * Execute text writing to current window 12841 * 12842 * @param {Integer} i Current index in the 708 packet 12843 * @param {Service} service The service object to be affected 12844 * @return {Integer} New index after parsing 12845 */ 12846 12847 12848 Cea708Stream.prototype.handleText = function (i, service, options) { 12849 var isExtended = options && options.isExtended; 12850 var isMultiByte = options && options.isMultiByte; 12851 var packetData = this.current708Packet.data; 12852 var extended = isExtended ? 0x1000 : 0x0000; 12853 var currentByte = packetData[i]; 12854 var nextByte = packetData[i + 1]; 12855 var win = service.currentWindow; 12856 var char; 12857 var charCodeArray; // Converts an array of bytes to a unicode hex string. 12858 12859 function toHexString(byteArray) { 12860 return byteArray.map(byte => { 12861 return ('0' + (byte & 0xFF).toString(16)).slice(-2); 12862 }).join(''); 12863 } 12864 12865 if (isMultiByte) { 12866 charCodeArray = [currentByte, nextByte]; 12867 i++; 12868 } else { 12869 charCodeArray = [currentByte]; 12870 } // Use the TextDecoder if one was created for this service 12871 12872 12873 if (service.textDecoder_ && !isExtended) { 12874 char = service.textDecoder_.decode(new Uint8Array(charCodeArray)); 12875 } else { 12876 // We assume any multi-byte char without a decoder is unicode. 12877 if (isMultiByte) { 12878 const unicode = toHexString(charCodeArray); // Takes a unicode hex string and creates a single character. 12879 12880 char = String.fromCharCode(parseInt(unicode, 16)); 12881 } else { 12882 char = get708CharFromCode(extended | currentByte); 12883 } 12884 } 12885 12886 if (win.pendingNewLine && !win.isEmpty()) { 12887 win.newLine(this.getPts(i)); 12888 } 12889 12890 win.pendingNewLine = false; 12891 win.addText(char); 12892 return i; 12893 }; 12894 /** 12895 * Handle decoding of multibyte character 12896 * 12897 * @param {Integer} i Current index in the 708 packet 12898 * @param {Service} service The service object to be affected 12899 * @return {Integer} New index after parsing 12900 */ 12901 12902 12903 Cea708Stream.prototype.multiByteCharacter = function (i, service) { 12904 var packetData = this.current708Packet.data; 12905 var firstByte = packetData[i + 1]; 12906 var secondByte = packetData[i + 2]; 12907 12908 if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) { 12909 i = this.handleText(++i, service, { 12910 isMultiByte: true 12911 }); 12912 } 12913 12914 return i; 12915 }; 12916 /** 12917 * Parse and execute the CW# command. 12918 * 12919 * Set the current window. 12920 * 12921 * @param {Integer} i Current index in the 708 packet 12922 * @param {Service} service The service object to be affected 12923 * @return {Integer} New index after parsing 12924 */ 12925 12926 12927 Cea708Stream.prototype.setCurrentWindow = function (i, service) { 12928 var packetData = this.current708Packet.data; 12929 var b = packetData[i]; 12930 var windowNum = b & 0x07; 12931 service.setCurrentWindow(windowNum); 12932 return i; 12933 }; 12934 /** 12935 * Parse and execute the DF# command. 12936 * 12937 * Define a window and set it as the current window. 12938 * 12939 * @param {Integer} i Current index in the 708 packet 12940 * @param {Service} service The service object to be affected 12941 * @return {Integer} New index after parsing 12942 */ 12943 12944 12945 Cea708Stream.prototype.defineWindow = function (i, service) { 12946 var packetData = this.current708Packet.data; 12947 var b = packetData[i]; 12948 var windowNum = b & 0x07; 12949 service.setCurrentWindow(windowNum); 12950 var win = service.currentWindow; 12951 b = packetData[++i]; 12952 win.visible = (b & 0x20) >> 5; // v 12953 12954 win.rowLock = (b & 0x10) >> 4; // rl 12955 12956 win.columnLock = (b & 0x08) >> 3; // cl 12957 12958 win.priority = b & 0x07; // p 12959 12960 b = packetData[++i]; 12961 win.relativePositioning = (b & 0x80) >> 7; // rp 12962 12963 win.anchorVertical = b & 0x7f; // av 12964 12965 b = packetData[++i]; 12966 win.anchorHorizontal = b; // ah 12967 12968 b = packetData[++i]; 12969 win.anchorPoint = (b & 0xf0) >> 4; // ap 12970 12971 win.rowCount = b & 0x0f; // rc 12972 12973 b = packetData[++i]; 12974 win.columnCount = b & 0x3f; // cc 12975 12976 b = packetData[++i]; 12977 win.windowStyle = (b & 0x38) >> 3; // ws 12978 12979 win.penStyle = b & 0x07; // ps 12980 // The spec says there are (rowCount+1) "virtual rows" 12981 12982 win.virtualRowCount = win.rowCount + 1; 12983 return i; 12984 }; 12985 /** 12986 * Parse and execute the SWA command. 12987 * 12988 * Set attributes of the current window. 12989 * 12990 * @param {Integer} i Current index in the 708 packet 12991 * @param {Service} service The service object to be affected 12992 * @return {Integer} New index after parsing 12993 */ 12994 12995 12996 Cea708Stream.prototype.setWindowAttributes = function (i, service) { 12997 var packetData = this.current708Packet.data; 12998 var b = packetData[i]; 12999 var winAttr = service.currentWindow.winAttr; 13000 b = packetData[++i]; 13001 winAttr.fillOpacity = (b & 0xc0) >> 6; // fo 13002 13003 winAttr.fillRed = (b & 0x30) >> 4; // fr 13004 13005 winAttr.fillGreen = (b & 0x0c) >> 2; // fg 13006 13007 winAttr.fillBlue = b & 0x03; // fb 13008 13009 b = packetData[++i]; 13010 winAttr.borderType = (b & 0xc0) >> 6; // bt 13011 13012 winAttr.borderRed = (b & 0x30) >> 4; // br 13013 13014 winAttr.borderGreen = (b & 0x0c) >> 2; // bg 13015 13016 winAttr.borderBlue = b & 0x03; // bb 13017 13018 b = packetData[++i]; 13019 winAttr.borderType += (b & 0x80) >> 5; // bt 13020 13021 winAttr.wordWrap = (b & 0x40) >> 6; // ww 13022 13023 winAttr.printDirection = (b & 0x30) >> 4; // pd 13024 13025 winAttr.scrollDirection = (b & 0x0c) >> 2; // sd 13026 13027 winAttr.justify = b & 0x03; // j 13028 13029 b = packetData[++i]; 13030 winAttr.effectSpeed = (b & 0xf0) >> 4; // es 13031 13032 winAttr.effectDirection = (b & 0x0c) >> 2; // ed 13033 13034 winAttr.displayEffect = b & 0x03; // de 13035 13036 return i; 13037 }; 13038 /** 13039 * Gather text from all displayed windows and push a caption to output. 13040 * 13041 * @param {Integer} i Current index in the 708 packet 13042 * @param {Service} service The service object to be affected 13043 */ 13044 13045 13046 Cea708Stream.prototype.flushDisplayed = function (pts, service) { 13047 var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily 13048 // display text in the correct order, but sample files so far have not shown any issue. 13049 13050 for (var winId = 0; winId < 8; winId++) { 13051 if (service.windows[winId].visible && !service.windows[winId].isEmpty()) { 13052 displayedText.push(service.windows[winId].getText()); 13053 } 13054 } 13055 13056 service.endPts = pts; 13057 service.text = displayedText.join('\n\n'); 13058 this.pushCaption(service); 13059 service.startPts = pts; 13060 }; 13061 /** 13062 * Push a caption to output if the caption contains text. 13063 * 13064 * @param {Service} service The service object to be affected 13065 */ 13066 13067 13068 Cea708Stream.prototype.pushCaption = function (service) { 13069 if (service.text !== '') { 13070 this.trigger('data', { 13071 startPts: service.startPts, 13072 endPts: service.endPts, 13073 text: service.text, 13074 stream: 'cc708_' + service.serviceNum 13075 }); 13076 service.text = ''; 13077 service.startPts = service.endPts; 13078 } 13079 }; 13080 /** 13081 * Parse and execute the DSW command. 13082 * 13083 * Set visible property of windows based on the parsed bitmask. 13084 * 13085 * @param {Integer} i Current index in the 708 packet 13086 * @param {Service} service The service object to be affected 13087 * @return {Integer} New index after parsing 13088 */ 13089 13090 13091 Cea708Stream.prototype.displayWindows = function (i, service) { 13092 var packetData = this.current708Packet.data; 13093 var b = packetData[++i]; 13094 var pts = this.getPts(i); 13095 this.flushDisplayed(pts, service); 13096 13097 for (var winId = 0; winId < 8; winId++) { 13098 if (b & 0x01 << winId) { 13099 service.windows[winId].visible = 1; 13100 } 13101 } 13102 13103 return i; 13104 }; 13105 /** 13106 * Parse and execute the HDW command. 13107 * 13108 * Set visible property of windows based on the parsed bitmask. 13109 * 13110 * @param {Integer} i Current index in the 708 packet 13111 * @param {Service} service The service object to be affected 13112 * @return {Integer} New index after parsing 13113 */ 13114 13115 13116 Cea708Stream.prototype.hideWindows = function (i, service) { 13117 var packetData = this.current708Packet.data; 13118 var b = packetData[++i]; 13119 var pts = this.getPts(i); 13120 this.flushDisplayed(pts, service); 13121 13122 for (var winId = 0; winId < 8; winId++) { 13123 if (b & 0x01 << winId) { 13124 service.windows[winId].visible = 0; 13125 } 13126 } 13127 13128 return i; 13129 }; 13130 /** 13131 * Parse and execute the TGW command. 13132 * 13133 * Set visible property of windows based on the parsed bitmask. 13134 * 13135 * @param {Integer} i Current index in the 708 packet 13136 * @param {Service} service The service object to be affected 13137 * @return {Integer} New index after parsing 13138 */ 13139 13140 13141 Cea708Stream.prototype.toggleWindows = function (i, service) { 13142 var packetData = this.current708Packet.data; 13143 var b = packetData[++i]; 13144 var pts = this.getPts(i); 13145 this.flushDisplayed(pts, service); 13146 13147 for (var winId = 0; winId < 8; winId++) { 13148 if (b & 0x01 << winId) { 13149 service.windows[winId].visible ^= 1; 13150 } 13151 } 13152 13153 return i; 13154 }; 13155 /** 13156 * Parse and execute the CLW command. 13157 * 13158 * Clear text of windows based on the parsed bitmask. 13159 * 13160 * @param {Integer} i Current index in the 708 packet 13161 * @param {Service} service The service object to be affected 13162 * @return {Integer} New index after parsing 13163 */ 13164 13165 13166 Cea708Stream.prototype.clearWindows = function (i, service) { 13167 var packetData = this.current708Packet.data; 13168 var b = packetData[++i]; 13169 var pts = this.getPts(i); 13170 this.flushDisplayed(pts, service); 13171 13172 for (var winId = 0; winId < 8; winId++) { 13173 if (b & 0x01 << winId) { 13174 service.windows[winId].clearText(); 13175 } 13176 } 13177 13178 return i; 13179 }; 13180 /** 13181 * Parse and execute the DLW command. 13182 * 13183 * Re-initialize windows based on the parsed bitmask. 13184 * 13185 * @param {Integer} i Current index in the 708 packet 13186 * @param {Service} service The service object to be affected 13187 * @return {Integer} New index after parsing 13188 */ 13189 13190 13191 Cea708Stream.prototype.deleteWindows = function (i, service) { 13192 var packetData = this.current708Packet.data; 13193 var b = packetData[++i]; 13194 var pts = this.getPts(i); 13195 this.flushDisplayed(pts, service); 13196 13197 for (var winId = 0; winId < 8; winId++) { 13198 if (b & 0x01 << winId) { 13199 service.windows[winId].reset(); 13200 } 13201 } 13202 13203 return i; 13204 }; 13205 /** 13206 * Parse and execute the SPA command. 13207 * 13208 * Set pen attributes of the current window. 13209 * 13210 * @param {Integer} i Current index in the 708 packet 13211 * @param {Service} service The service object to be affected 13212 * @return {Integer} New index after parsing 13213 */ 13214 13215 13216 Cea708Stream.prototype.setPenAttributes = function (i, service) { 13217 var packetData = this.current708Packet.data; 13218 var b = packetData[i]; 13219 var penAttr = service.currentWindow.penAttr; 13220 b = packetData[++i]; 13221 penAttr.textTag = (b & 0xf0) >> 4; // tt 13222 13223 penAttr.offset = (b & 0x0c) >> 2; // o 13224 13225 penAttr.penSize = b & 0x03; // s 13226 13227 b = packetData[++i]; 13228 penAttr.italics = (b & 0x80) >> 7; // i 13229 13230 penAttr.underline = (b & 0x40) >> 6; // u 13231 13232 penAttr.edgeType = (b & 0x38) >> 3; // et 13233 13234 penAttr.fontStyle = b & 0x07; // fs 13235 13236 return i; 13237 }; 13238 /** 13239 * Parse and execute the SPC command. 13240 * 13241 * Set pen color of the current window. 13242 * 13243 * @param {Integer} i Current index in the 708 packet 13244 * @param {Service} service The service object to be affected 13245 * @return {Integer} New index after parsing 13246 */ 13247 13248 13249 Cea708Stream.prototype.setPenColor = function (i, service) { 13250 var packetData = this.current708Packet.data; 13251 var b = packetData[i]; 13252 var penColor = service.currentWindow.penColor; 13253 b = packetData[++i]; 13254 penColor.fgOpacity = (b & 0xc0) >> 6; // fo 13255 13256 penColor.fgRed = (b & 0x30) >> 4; // fr 13257 13258 penColor.fgGreen = (b & 0x0c) >> 2; // fg 13259 13260 penColor.fgBlue = b & 0x03; // fb 13261 13262 b = packetData[++i]; 13263 penColor.bgOpacity = (b & 0xc0) >> 6; // bo 13264 13265 penColor.bgRed = (b & 0x30) >> 4; // br 13266 13267 penColor.bgGreen = (b & 0x0c) >> 2; // bg 13268 13269 penColor.bgBlue = b & 0x03; // bb 13270 13271 b = packetData[++i]; 13272 penColor.edgeRed = (b & 0x30) >> 4; // er 13273 13274 penColor.edgeGreen = (b & 0x0c) >> 2; // eg 13275 13276 penColor.edgeBlue = b & 0x03; // eb 13277 13278 return i; 13279 }; 13280 /** 13281 * Parse and execute the SPL command. 13282 * 13283 * Set pen location of the current window. 13284 * 13285 * @param {Integer} i Current index in the 708 packet 13286 * @param {Service} service The service object to be affected 13287 * @return {Integer} New index after parsing 13288 */ 13289 13290 13291 Cea708Stream.prototype.setPenLocation = function (i, service) { 13292 var packetData = this.current708Packet.data; 13293 var b = packetData[i]; 13294 var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak 13295 13296 service.currentWindow.pendingNewLine = true; 13297 b = packetData[++i]; 13298 penLoc.row = b & 0x0f; // r 13299 13300 b = packetData[++i]; 13301 penLoc.column = b & 0x3f; // c 13302 13303 return i; 13304 }; 13305 /** 13306 * Execute the RST command. 13307 * 13308 * Reset service to a clean slate. Re-initialize. 13309 * 13310 * @param {Integer} i Current index in the 708 packet 13311 * @param {Service} service The service object to be affected 13312 * @return {Service} Re-initialized service 13313 */ 13314 13315 13316 Cea708Stream.prototype.reset = function (i, service) { 13317 var pts = this.getPts(i); 13318 this.flushDisplayed(pts, service); 13319 return this.initService(service.serviceNum, i); 13320 }; // This hash maps non-ASCII, special, and extended character codes to their 13321 // proper Unicode equivalent. The first keys that are only a single byte 13322 // are the non-standard ASCII characters, which simply map the CEA608 byte 13323 // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 13324 // character codes, but have their MSB bitmasked with 0x03 so that a lookup 13325 // can be performed regardless of the field and data channel on which the 13326 // character code was received. 13327 13328 13329 var CHARACTER_TRANSLATION = { 13330 0x2a: 0xe1, 13331 // á 13332 0x5c: 0xe9, 13333 // é 13334 0x5e: 0xed, 13335 // à 13336 0x5f: 0xf3, 13337 // ó 13338 0x60: 0xfa, 13339 // ú 13340 0x7b: 0xe7, 13341 // ç 13342 0x7c: 0xf7, 13343 // ÷ 13344 0x7d: 0xd1, 13345 // à 13346 0x7e: 0xf1, 13347 // ñ 13348 0x7f: 0x2588, 13349 // â 13350 0x0130: 0xae, 13351 // ® 13352 0x0131: 0xb0, 13353 // ° 13354 0x0132: 0xbd, 13355 // ½ 13356 0x0133: 0xbf, 13357 // ¿ 13358 0x0134: 0x2122, 13359 // ⢠13360 0x0135: 0xa2, 13361 // ¢ 13362 0x0136: 0xa3, 13363 // £ 13364 0x0137: 0x266a, 13365 // ⪠13366 0x0138: 0xe0, 13367 // à 13368 0x0139: 0xa0, 13369 // 13370 0x013a: 0xe8, 13371 // è 13372 0x013b: 0xe2, 13373 // â 13374 0x013c: 0xea, 13375 // ê 13376 0x013d: 0xee, 13377 // î 13378 0x013e: 0xf4, 13379 // ô 13380 0x013f: 0xfb, 13381 // û 13382 0x0220: 0xc1, 13383 // à 13384 0x0221: 0xc9, 13385 // à 13386 0x0222: 0xd3, 13387 // à 13388 0x0223: 0xda, 13389 // à 13390 0x0224: 0xdc, 13391 // à 13392 0x0225: 0xfc, 13393 // ü 13394 0x0226: 0x2018, 13395 // â 13396 0x0227: 0xa1, 13397 // ¡ 13398 0x0228: 0x2a, 13399 // * 13400 0x0229: 0x27, 13401 // ' 13402 0x022a: 0x2014, 13403 // â 13404 0x022b: 0xa9, 13405 // © 13406 0x022c: 0x2120, 13407 // â 13408 0x022d: 0x2022, 13409 // ⢠13410 0x022e: 0x201c, 13411 // â 13412 0x022f: 0x201d, 13413 // â 13414 0x0230: 0xc0, 13415 // à 13416 0x0231: 0xc2, 13417 // à 13418 0x0232: 0xc7, 13419 // à 13420 0x0233: 0xc8, 13421 // à 13422 0x0234: 0xca, 13423 // à 13424 0x0235: 0xcb, 13425 // à 13426 0x0236: 0xeb, 13427 // ë 13428 0x0237: 0xce, 13429 // à 13430 0x0238: 0xcf, 13431 // à 13432 0x0239: 0xef, 13433 // ï 13434 0x023a: 0xd4, 13435 // à 13436 0x023b: 0xd9, 13437 // à 13438 0x023c: 0xf9, 13439 // ù 13440 0x023d: 0xdb, 13441 // à 13442 0x023e: 0xab, 13443 // « 13444 0x023f: 0xbb, 13445 // » 13446 0x0320: 0xc3, 13447 // à 13448 0x0321: 0xe3, 13449 // ã 13450 0x0322: 0xcd, 13451 // à 13452 0x0323: 0xcc, 13453 // à 13454 0x0324: 0xec, 13455 // ì 13456 0x0325: 0xd2, 13457 // à 13458 0x0326: 0xf2, 13459 // ò 13460 0x0327: 0xd5, 13461 // à 13462 0x0328: 0xf5, 13463 // õ 13464 0x0329: 0x7b, 13465 // { 13466 0x032a: 0x7d, 13467 // } 13468 0x032b: 0x5c, 13469 // \ 13470 0x032c: 0x5e, 13471 // ^ 13472 0x032d: 0x5f, 13473 // _ 13474 0x032e: 0x7c, 13475 // | 13476 0x032f: 0x7e, 13477 // ~ 13478 0x0330: 0xc4, 13479 // à 13480 0x0331: 0xe4, 13481 // ä 13482 0x0332: 0xd6, 13483 // à 13484 0x0333: 0xf6, 13485 // ö 13486 0x0334: 0xdf, 13487 // à 13488 0x0335: 0xa5, 13489 // Â¥ 13490 0x0336: 0xa4, 13491 // ¤ 13492 0x0337: 0x2502, 13493 // â 13494 0x0338: 0xc5, 13495 // à 13496 0x0339: 0xe5, 13497 // Ã¥ 13498 0x033a: 0xd8, 13499 // à 13500 0x033b: 0xf8, 13501 // ø 13502 0x033c: 0x250c, 13503 // â 13504 0x033d: 0x2510, 13505 // â 13506 0x033e: 0x2514, 13507 // â 13508 0x033f: 0x2518 // â 13509 13510 }; 13511 13512 var getCharFromCode = function (code) { 13513 if (code === null) { 13514 return ''; 13515 } 13516 13517 code = CHARACTER_TRANSLATION[code] || code; 13518 return String.fromCharCode(code); 13519 }; // the index of the last row in a CEA-608 display buffer 13520 13521 13522 var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of 13523 // getting it through bit logic. 13524 13525 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 13526 // cells. The "bottom" row is the last element in the outer array. 13527 // We keep track of positioning information as we go by storing the 13528 // number of indentations and the tab offset in this buffer. 13529 13530 var createDisplayBuffer = function () { 13531 var result = [], 13532 i = BOTTOM_ROW + 1; 13533 13534 while (i--) { 13535 result.push({ 13536 text: '', 13537 indent: 0, 13538 offset: 0 13539 }); 13540 } 13541 13542 return result; 13543 }; 13544 13545 var Cea608Stream = function (field, dataChannel) { 13546 Cea608Stream.prototype.init.call(this); 13547 this.field_ = field || 0; 13548 this.dataChannel_ = dataChannel || 0; 13549 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 13550 this.setConstants(); 13551 this.reset(); 13552 13553 this.push = function (packet) { 13554 var data, swap, char0, char1, text; // remove the parity bits 13555 13556 data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice 13557 13558 if (data === this.lastControlCode_) { 13559 this.lastControlCode_ = null; 13560 return; 13561 } // Store control codes 13562 13563 13564 if ((data & 0xf000) === 0x1000) { 13565 this.lastControlCode_ = data; 13566 } else if (data !== this.PADDING_) { 13567 this.lastControlCode_ = null; 13568 } 13569 13570 char0 = data >>> 8; 13571 char1 = data & 0xff; 13572 13573 if (data === this.PADDING_) { 13574 return; 13575 } else if (data === this.RESUME_CAPTION_LOADING_) { 13576 this.mode_ = 'popOn'; 13577 } else if (data === this.END_OF_CAPTION_) { 13578 // If an EOC is received while in paint-on mode, the displayed caption 13579 // text should be swapped to non-displayed memory as if it was a pop-on 13580 // caption. Because of that, we should explicitly switch back to pop-on 13581 // mode 13582 this.mode_ = 'popOn'; 13583 this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now 13584 13585 this.flushDisplayed(packet.pts); // flip memory 13586 13587 swap = this.displayed_; 13588 this.displayed_ = this.nonDisplayed_; 13589 this.nonDisplayed_ = swap; // start measuring the time to display the caption 13590 13591 this.startPts_ = packet.pts; 13592 } else if (data === this.ROLL_UP_2_ROWS_) { 13593 this.rollUpRows_ = 2; 13594 this.setRollUp(packet.pts); 13595 } else if (data === this.ROLL_UP_3_ROWS_) { 13596 this.rollUpRows_ = 3; 13597 this.setRollUp(packet.pts); 13598 } else if (data === this.ROLL_UP_4_ROWS_) { 13599 this.rollUpRows_ = 4; 13600 this.setRollUp(packet.pts); 13601 } else if (data === this.CARRIAGE_RETURN_) { 13602 this.clearFormatting(packet.pts); 13603 this.flushDisplayed(packet.pts); 13604 this.shiftRowsUp_(); 13605 this.startPts_ = packet.pts; 13606 } else if (data === this.BACKSPACE_) { 13607 if (this.mode_ === 'popOn') { 13608 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 13609 } else { 13610 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 13611 } 13612 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 13613 this.flushDisplayed(packet.pts); 13614 this.displayed_ = createDisplayBuffer(); 13615 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 13616 this.nonDisplayed_ = createDisplayBuffer(); 13617 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 13618 if (this.mode_ !== 'paintOn') { 13619 // NOTE: This should be removed when proper caption positioning is 13620 // implemented 13621 this.flushDisplayed(packet.pts); 13622 this.displayed_ = createDisplayBuffer(); 13623 } 13624 13625 this.mode_ = 'paintOn'; 13626 this.startPts_ = packet.pts; // Append special characters to caption text 13627 } else if (this.isSpecialCharacter(char0, char1)) { 13628 // Bitmask char0 so that we can apply character transformations 13629 // regardless of field and data channel. 13630 // Then byte-shift to the left and OR with char1 so we can pass the 13631 // entire character code to `getCharFromCode`. 13632 char0 = (char0 & 0x03) << 8; 13633 text = getCharFromCode(char0 | char1); 13634 this[this.mode_](packet.pts, text); 13635 this.column_++; // Append extended characters to caption text 13636 } else if (this.isExtCharacter(char0, char1)) { 13637 // Extended characters always follow their "non-extended" equivalents. 13638 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 13639 // decoders are supposed to drop the "è", while compliant decoders 13640 // backspace the "e" and insert "è". 13641 // Delete the previous character 13642 if (this.mode_ === 'popOn') { 13643 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 13644 } else { 13645 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 13646 } // Bitmask char0 so that we can apply character transformations 13647 // regardless of field and data channel. 13648 // Then byte-shift to the left and OR with char1 so we can pass the 13649 // entire character code to `getCharFromCode`. 13650 13651 13652 char0 = (char0 & 0x03) << 8; 13653 text = getCharFromCode(char0 | char1); 13654 this[this.mode_](packet.pts, text); 13655 this.column_++; // Process mid-row codes 13656 } else if (this.isMidRowCode(char0, char1)) { 13657 // Attributes are not additive, so clear all formatting 13658 this.clearFormatting(packet.pts); // According to the standard, mid-row codes 13659 // should be replaced with spaces, so add one now 13660 13661 this[this.mode_](packet.pts, ' '); 13662 this.column_++; 13663 13664 if ((char1 & 0xe) === 0xe) { 13665 this.addFormatting(packet.pts, ['i']); 13666 } 13667 13668 if ((char1 & 0x1) === 0x1) { 13669 this.addFormatting(packet.pts, ['u']); 13670 } // Detect offset control codes and adjust cursor 13671 13672 } else if (this.isOffsetControlCode(char0, char1)) { 13673 // Cursor position is set by indent PAC (see below) in 4-column 13674 // increments, with an additional offset code of 1-3 to reach any 13675 // of the 32 columns specified by CEA-608. So all we need to do 13676 // here is increment the column cursor by the given offset. 13677 const offset = char1 & 0x03; // For an offest value 1-3, set the offset for that caption 13678 // in the non-displayed array. 13679 13680 this.nonDisplayed_[this.row_].offset = offset; 13681 this.column_ += offset; // Detect PACs (Preamble Address Codes) 13682 } else if (this.isPAC(char0, char1)) { 13683 // There's no logic for PAC -> row mapping, so we have to just 13684 // find the row code in an array and use its index :( 13685 var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode 13686 13687 if (this.mode_ === 'rollUp') { 13688 // This implies that the base row is incorrectly set. 13689 // As per the recommendation in CEA-608(Base Row Implementation), defer to the number 13690 // of roll-up rows set. 13691 if (row - this.rollUpRows_ + 1 < 0) { 13692 row = this.rollUpRows_ - 1; 13693 } 13694 13695 this.setRollUp(packet.pts, row); 13696 }
13696 13697 13698 if (row !== this.row_
vendor: 83,471 bytes, lines 13698-16146
13698) { 13699 // formatting is only persistent for current row 13700 this.clearFormatting(packet.pts); 13701 this.row_ = row; 13702 } // All PACs can apply underline, so detect and apply 13703 // (All odd-numbered second bytes set underline) 13704 13705 13706 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 13707 this.addFormatting(packet.pts, ['u']); 13708 } 13709 13710 if ((data & 0x10) === 0x10) { 13711 // We've got an indent level code. Each successive even number 13712 // increments the column cursor by 4, so we can get the desired 13713 // column position by bit-shifting to the right (to get n/2) 13714 // and multiplying by 4. 13715 const indentations = (data & 0xe) >> 1; 13716 this.column_ = indentations * 4; // add to the number of indentations for positioning 13717 13718 this.nonDisplayed_[this.row_].indent += indentations; 13719 } 13720 13721 if (this.isColorPAC(char1)) { 13722 // it's a color code, though we only support white, which 13723 // can be either normal or italicized. white italics can be 13724 // either 0x4e or 0x6e depending on the row, so we just 13725 // bitwise-and with 0xe to see if italics should be turned on 13726 if ((char1 & 0xe) === 0xe) { 13727 this.addFormatting(packet.pts, ['i']); 13728 } 13729 } // We have a normal character in char0, and possibly one in char1 13730 13731 } else if (this.isNormalChar(char0)) { 13732 if (char1 === 0x00) { 13733 char1 = null; 13734 } 13735 13736 text = getCharFromCode(char0); 13737 text += getCharFromCode(char1); 13738 this[this.mode_](packet.pts, text); 13739 this.column_ += text.length; 13740 } // finish data processing 13741 13742 }; 13743 }; 13744 13745 Cea608Stream.prototype = new Stream$7(); // Trigger a cue point that captures the current state of the 13746 // display buffer 13747 13748 Cea608Stream.prototype.flushDisplayed = function (pts) { 13749 const logWarning = index => { 13750 this.trigger('log', { 13751 level: 'warn', 13752 message: 'Skipping a malformed 608 caption at index ' + index + '.' 13753 }); 13754 }; 13755 13756 const content = []; 13757 this.displayed_.forEach((row, i) => { 13758 if (row && row.text && row.text.length) { 13759 try { 13760 // remove spaces from the start and end of the string 13761 row.text = row.text.trim(); 13762 } catch (e) { 13763 // Ordinarily, this shouldn't happen. However, caption 13764 // parsing errors should not throw exceptions and 13765 // break playback. 13766 logWarning(i); 13767 } // See the below link for more details on the following fields: 13768 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 13769 13770 13771 if (row.text.length) { 13772 content.push({ 13773 // The text to be displayed in the caption from this specific row, with whitespace removed. 13774 text: row.text, 13775 // Value between 1 and 15 representing the PAC row used to calculate line height. 13776 line: i + 1, 13777 // A number representing the indent position by percentage (CEA-608 PAC indent code). 13778 // The value will be a number between 10 and 80. Offset is used to add an aditional 13779 // value to the position if necessary. 13780 position: 10 + Math.min(70, row.indent * 10) + row.offset * 2.5 13781 }); 13782 } 13783 } else if (row === undefined || row === null) { 13784 logWarning(i); 13785 } 13786 }); 13787 13788 if (content.length) { 13789 this.trigger('data', { 13790 startPts: this.startPts_, 13791 endPts: pts, 13792 content, 13793 stream: this.name_ 13794 }); 13795 } 13796 }; 13797 /** 13798 * Zero out the data, used for startup and on seek 13799 */ 13800 13801 13802 Cea608Stream.prototype.reset = function () { 13803 this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will 13804 // actually display captions. If a caption is shifted to a row 13805 // with a lower index than this, it is cleared from the display 13806 // buffer 13807 13808 this.topRow_ = 0; 13809 this.startPts_ = 0; 13810 this.displayed_ = createDisplayBuffer(); 13811 this.nonDisplayed_ = createDisplayBuffer(); 13812 this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing 13813 13814 this.column_ = 0; 13815 this.row_ = BOTTOM_ROW; 13816 this.rollUpRows_ = 2; // This variable holds currently-applied formatting 13817 13818 this.formatting_ = []; 13819 }; 13820 /** 13821 * Sets up control code and related constants for this instance 13822 */ 13823 13824 13825 Cea608Stream.prototype.setConstants = function () { 13826 // The following attributes have these uses: 13827 // ext_ : char0 for mid-row codes, and the base for extended 13828 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 13829 // extended codes) 13830 // control_: char0 for control codes, except byte-shifted to the 13831 // left so that we can do this.control_ | CONTROL_CODE 13832 // offset_: char0 for tab offset codes 13833 // 13834 // It's also worth noting that control codes, and _only_ control codes, 13835 // differ between field 1 and field2. Field 2 control codes are always 13836 // their field 1 value plus 1. That's why there's the "| field" on the 13837 // control value. 13838 if (this.dataChannel_ === 0) { 13839 this.BASE_ = 0x10; 13840 this.EXT_ = 0x11; 13841 this.CONTROL_ = (0x14 | this.field_) << 8; 13842 this.OFFSET_ = 0x17; 13843 } else if (this.dataChannel_ === 1) { 13844 this.BASE_ = 0x18; 13845 this.EXT_ = 0x19; 13846 this.CONTROL_ = (0x1c | this.field_) << 8; 13847 this.OFFSET_ = 0x1f; 13848 } // Constants for the LSByte command codes recognized by Cea608Stream. This 13849 // list is not exhaustive. For a more comprehensive listing and semantics see 13850 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 13851 // Padding 13852 13853 13854 this.PADDING_ = 0x0000; // Pop-on Mode 13855 13856 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 13857 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode 13858 13859 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 13860 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 13861 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 13862 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode 13863 13864 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure 13865 13866 this.BACKSPACE_ = this.CONTROL_ | 0x21; 13867 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 13868 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 13869 }; 13870 /** 13871 * Detects if the 2-byte packet data is a special character 13872 * 13873 * Special characters have a second byte in the range 0x30 to 0x3f, 13874 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 13875 * data channel 2). 13876 * 13877 * @param {Integer} char0 The first byte 13878 * @param {Integer} char1 The second byte 13879 * @return {Boolean} Whether the 2 bytes are an special character 13880 */ 13881 13882 13883 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 13884 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 13885 }; 13886 /** 13887 * Detects if the 2-byte packet data is an extended character 13888 * 13889 * Extended characters have a second byte in the range 0x20 to 0x3f, 13890 * with the first byte being 0x12 or 0x13 (for data channel 1) or 13891 * 0x1a or 0x1b (for data channel 2). 13892 * 13893 * @param {Integer} char0 The first byte 13894 * @param {Integer} char1 The second byte 13895 * @return {Boolean} Whether the 2 bytes are an extended character 13896 */ 13897 13898 13899 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 13900 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 13901 }; 13902 /** 13903 * Detects if the 2-byte packet is a mid-row code 13904 * 13905 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 13906 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 13907 * channel 2). 13908 * 13909 * @param {Integer} char0 The first byte 13910 * @param {Integer} char1 The second byte 13911 * @return {Boolean} Whether the 2 bytes are a mid-row code 13912 */ 13913 13914 13915 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 13916 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 13917 }; 13918 /** 13919 * Detects if the 2-byte packet is an offset control code 13920 * 13921 * Offset control codes have a second byte in the range 0x21 to 0x23, 13922 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 13923 * data channel 2). 13924 * 13925 * @param {Integer} char0 The first byte 13926 * @param {Integer} char1 The second byte 13927 * @return {Boolean} Whether the 2 bytes are an offset control code 13928 */ 13929 13930 13931 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 13932 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 13933 }; 13934 /** 13935 * Detects if the 2-byte packet is a Preamble Address Code 13936 * 13937 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 13938 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 13939 * range 0x40 to 0x7f. 13940 * 13941 * @param {Integer} char0 The first byte 13942 * @param {Integer} char1 The second byte 13943 * @return {Boolean} Whether the 2 bytes are a PAC 13944 */ 13945 13946 13947 Cea608Stream.prototype.isPAC = function (char0, char1) { 13948 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 13949 }; 13950 /** 13951 * Detects if a packet's second byte is in the range of a PAC color code 13952 * 13953 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 13954 * 0x60 to 0x6f. 13955 * 13956 * @param {Integer} char1 The second byte 13957 * @return {Boolean} Whether the byte is a color PAC 13958 */ 13959 13960 13961 Cea608Stream.prototype.isColorPAC = function (char1) { 13962 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 13963 }; 13964 /** 13965 * Detects if a single byte is in the range of a normal character 13966 * 13967 * Normal text bytes are in the range 0x20 to 0x7f. 13968 * 13969 * @param {Integer} char The byte 13970 * @return {Boolean} Whether the byte is a normal character 13971 */ 13972 13973 13974 Cea608Stream.prototype.isNormalChar = function (char) { 13975 return char >= 0x20 && char <= 0x7f; 13976 }; 13977 /** 13978 * Configures roll-up 13979 * 13980 * @param {Integer} pts Current PTS 13981 * @param {Integer} newBaseRow Used by PACs to slide the current window to 13982 * a new position 13983 */ 13984 13985 13986 Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) { 13987 // Reset the base row to the bottom row when switching modes 13988 if (this.mode_ !== 'rollUp') { 13989 this.row_ = BOTTOM_ROW; 13990 this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up 13991 13992 this.flushDisplayed(pts); 13993 this.nonDisplayed_ = createDisplayBuffer(); 13994 this.displayed_ = createDisplayBuffer(); 13995 } 13996 13997 if (newBaseRow !== undefined && newBaseRow !== this.row_) { 13998 // move currently displayed captions (up or down) to the new base row 13999 for (var i = 0; i < this.rollUpRows_; i++) { 14000 this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; 14001 this.displayed_[this.row_ - i] = { 14002 text: '', 14003 indent: 0, 14004 offset: 0 14005 }; 14006 } 14007 } 14008 14009 if (newBaseRow === undefined) { 14010 newBaseRow = this.row_; 14011 } 14012 14013 this.topRow_ = newBaseRow - this.rollUpRows_ + 1; 14014 }; // Adds the opening HTML tag for the passed character to the caption text, 14015 // and keeps track of it for later closing 14016 14017 14018 Cea608Stream.prototype.addFormatting = function (pts, format) { 14019 this.formatting_ = this.formatting_.concat(format); 14020 var text = format.reduce(function (text, format) { 14021 return text + '<' + format + '>'; 14022 }, ''); 14023 this[this.mode_](pts, text); 14024 }; // Adds HTML closing tags for current formatting to caption text and 14025 // clears remembered formatting 14026 14027 14028 Cea608Stream.prototype.clearFormatting = function (pts) { 14029 if (!this.formatting_.length) { 14030 return; 14031 } 14032 14033 var text = this.formatting_.reverse().reduce(function (text, format) { 14034 return text + '</' + format + '>'; 14035 }, ''); 14036 this.formatting_ = []; 14037 this[this.mode_](pts, text); 14038 }; // Mode Implementations 14039 14040 14041 Cea608Stream.prototype.popOn = function (pts, text) { 14042 var baseRow = this.nonDisplayed_[this.row_].text; // buffer characters 14043 14044 baseRow += text; 14045 this.nonDisplayed_[this.row_].text = baseRow; 14046 }; 14047 14048 Cea608Stream.prototype.rollUp = function (pts, text) { 14049 var baseRow = this.displayed_[this.row_].text; 14050 baseRow += text; 14051 this.displayed_[this.row_].text = baseRow; 14052 }; 14053 14054 Cea608Stream.prototype.shiftRowsUp_ = function () { 14055 var i; // clear out inactive rows 14056 14057 for (i = 0; i < this.topRow_; i++) { 14058 this.displayed_[i] = { 14059 text: '', 14060 indent: 0, 14061 offset: 0 14062 }; 14063 } 14064 14065 for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { 14066 this.displayed_[i] = { 14067 text: '', 14068 indent: 0, 14069 offset: 0 14070 }; 14071 } // shift displayed rows up 14072 14073 14074 for (i = this.topRow_; i < this.row_; i++) { 14075 this.displayed_[i] = this.displayed_[i + 1]; 14076 } // clear out the bottom row 14077 14078 14079 this.displayed_[this.row_] = { 14080 text: '', 14081 indent: 0, 14082 offset: 0 14083 }; 14084 }; 14085 14086 Cea608Stream.prototype.paintOn = function (pts, text) { 14087 var baseRow = this.displayed_[this.row_].text; 14088 baseRow += text; 14089 this.displayed_[this.row_].text = baseRow; 14090 }; // exports 14091 14092 14093 var captionStream = { 14094 CaptionStream: CaptionStream$2, 14095 Cea608Stream: Cea608Stream, 14096 Cea708Stream: Cea708Stream 14097 }; 14098 /** 14099 * mux.js 14100 * 14101 * Copyright (c) Brightcove 14102 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14103 */ 14104 14105 var streamTypes = { 14106 H264_STREAM_TYPE: 0x1B, 14107 ADTS_STREAM_TYPE: 0x0F, 14108 METADATA_STREAM_TYPE: 0x15 14109 }; 14110 /** 14111 * mux.js 14112 * 14113 * Copyright (c) Brightcove 14114 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14115 * 14116 * Accepts program elementary stream (PES) data events and corrects 14117 * decode and presentation time stamps to account for a rollover 14118 * of the 33 bit value. 14119 */ 14120 14121 var Stream$6 = stream; 14122 var MAX_TS = 8589934592; 14123 var RO_THRESH = 4294967296; 14124 var TYPE_SHARED = 'shared'; 14125 14126 var handleRollover$1 = function (value, reference) { 14127 var direction = 1; 14128 14129 if (value > reference) { 14130 // If the current timestamp value is greater than our reference timestamp and we detect a 14131 // timestamp rollover, this means the roll over is happening in the opposite direction. 14132 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 14133 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 14134 // rollover point. In loading this segment, the timestamp values will be very large, 14135 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 14136 // the time stamp to be `value - 2^33`. 14137 direction = -1; 14138 } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 14139 // cause an incorrect adjustment. 14140 14141 14142 while (Math.abs(reference - value) > RO_THRESH) { 14143 value += direction * MAX_TS; 14144 } 14145 14146 return value; 14147 }; 14148 14149 var TimestampRolloverStream$1 = function (type) { 14150 var lastDTS, referenceDTS; 14151 TimestampRolloverStream$1.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed 14152 // video and audio. We could use `undefined` here, but having a string 14153 // makes debugging a little clearer. 14154 14155 this.type_ = type || TYPE_SHARED; 14156 14157 this.push = function (data) { 14158 /** 14159 * Rollover stream expects data from elementary stream. 14160 * Elementary stream can push forward 2 types of data 14161 * - Parsed Video/Audio/Timed-metadata PES (packetized elementary stream) packets 14162 * - Tracks metadata from PMT (Program Map Table) 14163 * Rollover stream expects pts/dts info to be available, since it stores lastDTS 14164 * We should ignore non-PES packets since they may override lastDTS to undefined. 14165 * lastDTS is important to signal the next segments 14166 * about rollover from the previous segments. 14167 */ 14168 if (data.type === 'metadata') { 14169 this.trigger('data', data); 14170 return; 14171 } // Any "shared" rollover streams will accept _all_ data. Otherwise, 14172 // streams will only accept data that matches their type. 14173 14174 14175 if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { 14176 return; 14177 } 14178 14179 if (referenceDTS === undefined) { 14180 referenceDTS = data.dts; 14181 } 14182 14183 data.dts = handleRollover$1(data.dts, referenceDTS); 14184 data.pts = handleRollover$1(data.pts, referenceDTS); 14185 lastDTS = data.dts; 14186 this.trigger('data', data); 14187 }; 14188 14189 this.flush = function () { 14190 referenceDTS = lastDTS; 14191 this.trigger('done'); 14192 }; 14193 14194 this.endTimeline = function () { 14195 this.flush(); 14196 this.trigger('endedtimeline'); 14197 }; 14198 14199 this.discontinuity = function () { 14200 referenceDTS = void 0; 14201 lastDTS = void 0; 14202 }; 14203 14204 this.reset = function () { 14205 this.discontinuity(); 14206 this.trigger('reset'); 14207 }; 14208 }; 14209 14210 TimestampRolloverStream$1.prototype = new Stream$6(); 14211 var timestampRolloverStream = { 14212 TimestampRolloverStream: TimestampRolloverStream$1, 14213 handleRollover: handleRollover$1 14214 }; // Once IE11 support is dropped, this function should be removed. 14215 14216 var typedArrayIndexOf$1 = (typedArray, element, fromIndex) => { 14217 if (!typedArray) { 14218 return -1; 14219 } 14220 14221 var currentIndex = fromIndex; 14222 14223 for (; currentIndex < typedArray.length; currentIndex++) { 14224 if (typedArray[currentIndex] === element) { 14225 return currentIndex; 14226 } 14227 } 14228 14229 return -1; 14230 }; 14231 14232 var typedArray = { 14233 typedArrayIndexOf: typedArrayIndexOf$1 14234 }; 14235 /** 14236 * mux.js 14237 * 14238 * Copyright (c) Brightcove 14239 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14240 * 14241 * Tools for parsing ID3 frame data 14242 * @see http://id3.org/id3v2.3.0 14243 */ 14244 14245 var typedArrayIndexOf = typedArray.typedArrayIndexOf, 14246 // Frames that allow different types of text encoding contain a text 14247 // encoding description byte [ID3v2.4.0 section 4.] 14248 textEncodingDescriptionByte = { 14249 Iso88591: 0x00, 14250 // ISO-8859-1, terminated with \0. 14251 Utf16: 0x01, 14252 // UTF-16 encoded Unicode BOM, terminated with \0\0 14253 Utf16be: 0x02, 14254 // UTF-16BE encoded Unicode, without BOM, terminated with \0\0 14255 Utf8: 0x03 // UTF-8 encoded Unicode, terminated with \0 14256 14257 }, 14258 // return a percent-encoded representation of the specified byte range 14259 // @see http://en.wikipedia.org/wiki/Percent-encoding 14260 percentEncode$1 = function (bytes, start, end) { 14261 var i, 14262 result = ''; 14263 14264 for (i = start; i < end; i++) { 14265 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 14266 } 14267 14268 return result; 14269 }, 14270 // return the string representation of the specified byte range, 14271 // interpreted as UTf-8. 14272 parseUtf8 = function (bytes, start, end) { 14273 return decodeURIComponent(percentEncode$1(bytes, start, end)); 14274 }, 14275 // return the string representation of the specified byte range, 14276 // interpreted as ISO-8859-1. 14277 parseIso88591$1 = function (bytes, start, end) { 14278 return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line 14279 }, 14280 parseSyncSafeInteger$1 = function (data) { 14281 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 14282 }, 14283 frameParsers = { 14284 'APIC': function (frame) { 14285 var i = 1, 14286 mimeTypeEndIndex, 14287 descriptionEndIndex, 14288 LINK_MIME_TYPE = '-->'; 14289 14290 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14291 // ignore frames with unrecognized character encodings 14292 return; 14293 } // parsing fields [ID3v2.4.0 section 4.14.] 14294 14295 14296 mimeTypeEndIndex = typedArrayIndexOf(frame.data, 0, i); 14297 14298 if (mimeTypeEndIndex < 0) { 14299 // malformed frame 14300 return; 14301 } // parsing Mime type field (terminated with \0) 14302 14303 14304 frame.mimeType = parseIso88591$1(frame.data, i, mimeTypeEndIndex); 14305 i = mimeTypeEndIndex + 1; // parsing 1-byte Picture Type field 14306 14307 frame.pictureType = frame.data[i]; 14308 i++; 14309 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, i); 14310 14311 if (descriptionEndIndex < 0) { 14312 // malformed frame 14313 return; 14314 } // parsing Description field (terminated with \0) 14315 14316 14317 frame.description = parseUtf8(frame.data, i, descriptionEndIndex); 14318 i = descriptionEndIndex + 1; 14319 14320 if (frame.mimeType === LINK_MIME_TYPE) { 14321 // parsing Picture Data field as URL (always represented as ISO-8859-1 [ID3v2.4.0 section 4.]) 14322 frame.url = parseIso88591$1(frame.data, i, frame.data.length); 14323 } else { 14324 // parsing Picture Data field as binary data 14325 frame.pictureData = frame.data.subarray(i, frame.data.length); 14326 } 14327 }, 14328 'T*': function (frame) { 14329 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14330 // ignore frames with unrecognized character encodings 14331 return; 14332 } // parse text field, do not include null terminator in the frame value 14333 // frames that allow different types of encoding contain terminated text [ID3v2.4.0 section 4.] 14334 14335 14336 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.] 14337 14338 frame.values = frame.value.split('\0'); 14339 }, 14340 'TXXX': function (frame) { 14341 var descriptionEndIndex; 14342 14343 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14344 // ignore frames with unrecognized character encodings 14345 return; 14346 } 14347 14348 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 14349 14350 if (descriptionEndIndex === -1) { 14351 return; 14352 } // parse the text fields 14353 14354 14355 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // do not include the null terminator in the tag value 14356 // frames that allow different types of encoding contain terminated text 14357 // [ID3v2.4.0 section 4.] 14358 14359 frame.value = parseUtf8(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0*$/, ''); 14360 frame.data = frame.value; 14361 }, 14362 'W*': function (frame) { 14363 // parse URL field; URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 14364 // if the value is followed by a string termination all the following information should be ignored [ID3v2.4.0 section 4.3] 14365 frame.url = parseIso88591$1(frame.data, 0, frame.data.length).replace(/\0.*$/, ''); 14366 }, 14367 'WXXX': function (frame) { 14368 var descriptionEndIndex; 14369 14370 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 14371 // ignore frames with unrecognized character encodings 14372 return; 14373 } 14374 14375 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 14376 14377 if (descriptionEndIndex === -1) { 14378 return; 14379 } // parse the description and URL fields 14380 14381 14382 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 14383 // if the value is followed by a string termination all the following information 14384 // should be ignored [ID3v2.4.0 section 4.3] 14385 14386 frame.url = parseIso88591$1(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0.*$/, ''); 14387 }, 14388 'PRIV': function (frame) { 14389 var i; 14390 14391 for (i = 0; i < frame.data.length; i++) { 14392 if (frame.data[i] === 0) { 14393 // parse the description and URL fields 14394 frame.owner = parseIso88591$1(frame.data, 0, i); 14395 break; 14396 } 14397 } 14398 14399 frame.privateData = frame.data.subarray(i + 1); 14400 frame.data = frame.privateData; 14401 } 14402 }; 14403 14404 var parseId3Frames$1 = function (data) { 14405 var frameSize, 14406 frameHeader, 14407 frameStart = 10, 14408 tagSize = 0, 14409 frames = []; // If we don't have enough data for a header, 10 bytes, 14410 // or 'ID3' in the first 3 bytes this is not a valid ID3 tag. 14411 14412 if (data.length < 10 || data[0] !== 'I'.charCodeAt(0) || data[1] !== 'D'.charCodeAt(0) || data[2] !== '3'.charCodeAt(0)) { 14413 return; 14414 } // the frame size is transmitted as a 28-bit integer in the 14415 // last four bytes of the ID3 header. 14416 // The most significant bit of each byte is dropped and the 14417 // results concatenated to recover the actual value. 14418 14419 14420 tagSize = parseSyncSafeInteger$1(data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 14421 // convenient for our comparisons to include it 14422 14423 tagSize += 10; // check bit 6 of byte 5 for the extended header flag. 14424 14425 var hasExtendedHeader = data[5] & 0x40; 14426 14427 if (hasExtendedHeader) { 14428 // advance the frame start past the extended header 14429 frameStart += 4; // header size field 14430 14431 frameStart += parseSyncSafeInteger$1(data.subarray(10, 14)); 14432 tagSize -= parseSyncSafeInteger$1(data.subarray(16, 20)); // clip any padding off the end 14433 } // parse one or more ID3 frames 14434 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 14435 14436 14437 do { 14438 // determine the number of bytes in this frame 14439 frameSize = parseSyncSafeInteger$1(data.subarray(frameStart + 4, frameStart + 8)); 14440 14441 if (frameSize < 1) { 14442 break; 14443 } 14444 14445 frameHeader = String.fromCharCode(data[frameStart], data[frameStart + 1], data[frameStart + 2], data[frameStart + 3]); 14446 var frame = { 14447 id: frameHeader, 14448 data: data.subarray(frameStart + 10, frameStart + frameSize + 10) 14449 }; 14450 frame.key = frame.id; // parse frame values 14451 14452 if (frameParsers[frame.id]) { 14453 // use frame specific parser 14454 frameParsers[frame.id](frame); 14455 } else if (frame.id[0] === 'T') { 14456 // use text frame generic parser 14457 frameParsers['T*'](frame); 14458 } else if (frame.id[0] === 'W') { 14459 // use URL link frame generic parser 14460 frameParsers['W*'](frame); 14461 } 14462 14463 frames.push(frame); 14464 frameStart += 10; // advance past the frame header 14465 14466 frameStart += frameSize; // advance past the frame body 14467 } while (frameStart < tagSize); 14468 14469 return frames; 14470 }; 14471 14472 var parseId3 = { 14473 parseId3Frames: parseId3Frames$1, 14474 parseSyncSafeInteger: parseSyncSafeInteger$1, 14475 frameParsers: frameParsers 14476 }; 14477 /** 14478 * mux.js 14479 * 14480 * Copyright (c) Brightcove 14481 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14482 * 14483 * Accepts program elementary stream (PES) data events and parses out 14484 * ID3 metadata from them, if present. 14485 * @see http://id3.org/id3v2.3.0 14486 */ 14487 14488 var Stream$5 = stream, 14489 StreamTypes$3 = streamTypes, 14490 id3 = parseId3, 14491 MetadataStream; 14492 14493 MetadataStream = function (options) { 14494 var settings = { 14495 // the bytes of the program-level descriptor field in MP2T 14496 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 14497 // program element descriptors" 14498 descriptor: options && options.descriptor 14499 }, 14500 // the total size in bytes of the ID3 tag being parsed 14501 tagSize = 0, 14502 // tag data that is not complete enough to be parsed 14503 buffer = [], 14504 // the total number of bytes currently in the buffer 14505 bufferSize = 0, 14506 i; 14507 MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type 14508 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 14509 14510 this.dispatchType = StreamTypes$3.METADATA_STREAM_TYPE.toString(16); 14511 14512 if (settings.descriptor) { 14513 for (i = 0; i < settings.descriptor.length; i++) { 14514 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 14515 } 14516 } 14517 14518 this.push = function (chunk) { 14519 var tag, frameStart, frameSize, frame, i, frameHeader; 14520 14521 if (chunk.type !== 'timed-metadata') { 14522 return; 14523 } // if data_alignment_indicator is set in the PES header, 14524 // we must have the start of a new ID3 tag. Assume anything 14525 // remaining in the buffer was malformed and throw it out 14526 14527 14528 if (chunk.dataAlignmentIndicator) { 14529 bufferSize = 0; 14530 buffer.length = 0; 14531 } // ignore events that don't look like ID3 data 14532 14533 14534 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))) { 14535 this.trigger('log', { 14536 level: 'warn', 14537 message: 'Skipping unrecognized metadata packet' 14538 }); 14539 return; 14540 } // add this chunk to the data we've collected so far 14541 14542 14543 buffer.push(chunk); 14544 bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header 14545 14546 if (buffer.length === 1) { 14547 // the frame size is transmitted as a 28-bit integer in the 14548 // last four bytes of the ID3 header. 14549 // The most significant bit of each byte is dropped and the 14550 // results concatenated to recover the actual value. 14551 tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 14552 // convenient for our comparisons to include it 14553 14554 tagSize += 10; 14555 } // if the entire frame has not arrived, wait for more data 14556 14557 14558 if (bufferSize < tagSize) { 14559 return; 14560 } // collect the entire frame so it can be parsed 14561 14562 14563 tag = { 14564 data: new Uint8Array(tagSize), 14565 frames: [], 14566 pts: buffer[0].pts, 14567 dts: buffer[0].dts 14568 }; 14569 14570 for (i = 0; i < tagSize;) { 14571 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 14572 i += buffer[0].data.byteLength; 14573 bufferSize -= buffer[0].data.byteLength; 14574 buffer.shift(); 14575 } // find the start of the first frame and the end of the tag 14576 14577 14578 frameStart = 10; 14579 14580 if (tag.data[5] & 0x40) { 14581 // advance the frame start past the extended header 14582 frameStart += 4; // header size field 14583 14584 frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end 14585 14586 tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20)); 14587 } // parse one or more ID3 frames 14588 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 14589 14590 14591 do { 14592 // determine the number of bytes in this frame 14593 frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 14594 14595 if (frameSize < 1) { 14596 this.trigger('log', { 14597 level: 'warn', 14598 message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.' 14599 }); // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames 14600 // to be sent along. 14601 14602 break; 14603 } 14604 14605 frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]); 14606 frame = { 14607 id: frameHeader, 14608 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) 14609 }; 14610 frame.key = frame.id; // parse frame values 14611 14612 if (id3.frameParsers[frame.id]) { 14613 // use frame specific parser 14614 id3.frameParsers[frame.id](frame); 14615 } else if (frame.id[0] === 'T') { 14616 // use text frame generic parser 14617 id3.frameParsers['T*'](frame); 14618 } else if (frame.id[0] === 'W') { 14619 // use URL link frame generic parser 14620 id3.frameParsers['W*'](frame); 14621 } // handle the special PRIV frame used to indicate the start 14622 // time for raw AAC data 14623 14624 14625 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 14626 var d = frame.data, 14627 size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 14628 size *= 4; 14629 size += d[7] & 0x03; 14630 frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based 14631 // on the value of this frame 14632 // we couldn't have known the appropriate pts and dts before 14633 // parsing this ID3 tag so set those values now 14634 14635 if (tag.pts === undefined && tag.dts === undefined) { 14636 tag.pts = frame.timeStamp; 14637 tag.dts = frame.timeStamp; 14638 } 14639 14640 this.trigger('timestamp', frame); 14641 } 14642 14643 tag.frames.push(frame); 14644 frameStart += 10; // advance past the frame header 14645 14646 frameStart += frameSize; // advance past the frame body 14647 } while (frameStart < tagSize); 14648 14649 this.trigger('data', tag); 14650 }; 14651 }; 14652 14653 MetadataStream.prototype = new Stream$5(); 14654 var metadataStream = MetadataStream; 14655 /** 14656 * mux.js 14657 * 14658 * Copyright (c) Brightcove 14659 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 14660 * 14661 * A stream-based mp2t to mp4 converter. This utility can be used to 14662 * deliver mp4s to a SourceBuffer on platforms that support native 14663 * Media Source Extensions. 14664 */ 14665 14666 var Stream$4 = stream, 14667 CaptionStream$1 = captionStream, 14668 StreamTypes$2 = streamTypes, 14669 TimestampRolloverStream = timestampRolloverStream.TimestampRolloverStream; // object types 14670 14671 var TransportPacketStream, TransportParseStream, ElementaryStream; // constants 14672 14673 var MP2T_PACKET_LENGTH$1 = 188, 14674 // bytes 14675 SYNC_BYTE$1 = 0x47; 14676 /** 14677 * Splits an incoming stream of binary data into MPEG-2 Transport 14678 * Stream packets. 14679 */ 14680 14681 TransportPacketStream = function () { 14682 var buffer = new Uint8Array(MP2T_PACKET_LENGTH$1), 14683 bytesInBuffer = 0; 14684 TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream. 14685 14686 /** 14687 * Split a stream of data into M2TS packets 14688 **/ 14689 14690 this.push = function (bytes) { 14691 var startIndex = 0, 14692 endIndex = MP2T_PACKET_LENGTH$1, 14693 everything; // If there are bytes remaining from the last segment, prepend them to the 14694 // bytes that were pushed in 14695 14696 if (bytesInBuffer) { 14697 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 14698 everything.set(buffer.subarray(0, bytesInBuffer)); 14699 everything.set(bytes, bytesInBuffer); 14700 bytesInBuffer = 0; 14701 } else { 14702 everything = bytes; 14703 } // While we have enough data for a packet 14704 14705 14706 while (endIndex < everything.byteLength) { 14707 // Look for a pair of start and end sync bytes in the data.. 14708 if (everything[startIndex] === SYNC_BYTE$1 && everything[endIndex] === SYNC_BYTE$1) { 14709 // We found a packet so emit it and jump one whole packet forward in 14710 // the stream 14711 this.trigger('data', everything.subarray(startIndex, endIndex)); 14712 startIndex += MP2T_PACKET_LENGTH$1; 14713 endIndex += MP2T_PACKET_LENGTH$1; 14714 continue; 14715 } // If we get here, we have somehow become de-synchronized and we need to step 14716 // forward one byte at a time until we find a pair of sync bytes that denote 14717 // a packet 14718 14719 14720 startIndex++; 14721 endIndex++; 14722 } // If there was some data left over at the end of the segment that couldn't 14723 // possibly be a whole packet, keep it because it might be the start of a packet 14724 // that continues in the next segment 14725 14726 14727 if (startIndex < everything.byteLength) { 14728 buffer.set(everything.subarray(startIndex), 0); 14729 bytesInBuffer = everything.byteLength - startIndex; 14730 } 14731 }; 14732 /** 14733 * Passes identified M2TS packets to the TransportParseStream to be parsed 14734 **/ 14735 14736 14737 this.flush = function () { 14738 // If the buffer contains a whole packet when we are being flushed, emit it 14739 // and empty the buffer. Otherwise hold onto the data because it may be 14740 // important for decoding the next segment 14741 if (bytesInBuffer === MP2T_PACKET_LENGTH$1 && buffer[0] === SYNC_BYTE$1) { 14742 this.trigger('data', buffer); 14743 bytesInBuffer = 0; 14744 } 14745 14746 this.trigger('done'); 14747 }; 14748 14749 this.endTimeline = function () { 14750 this.flush(); 14751 this.trigger('endedtimeline'); 14752 }; 14753 14754 this.reset = function () { 14755 bytesInBuffer = 0; 14756 this.trigger('reset'); 14757 }; 14758 }; 14759 14760 TransportPacketStream.prototype = new Stream$4(); 14761 /** 14762 * Accepts an MP2T TransportPacketStream and emits data events with parsed 14763 * forms of the individual transport stream packets. 14764 */ 14765 14766 TransportParseStream = function () { 14767 var parsePsi, parsePat, parsePmt, self; 14768 TransportParseStream.prototype.init.call(this); 14769 self = this; 14770 this.packetsWaitingForPmt = []; 14771 this.programMapTable = undefined; 14772 14773 parsePsi = function (payload, psi) { 14774 var offset = 0; // PSI packets may be split into multiple sections and those 14775 // sections may be split into multiple packets. If a PSI 14776 // section starts in this packet, the payload_unit_start_indicator 14777 // will be true and the first byte of the payload will indicate 14778 // the offset from the current position to the start of the 14779 // section. 14780 14781 if (psi.payloadUnitStartIndicator) { 14782 offset += payload[offset] + 1; 14783 } 14784 14785 if (psi.type === 'pat') { 14786 parsePat(payload.subarray(offset), psi); 14787 } else { 14788 parsePmt(payload.subarray(offset), psi); 14789 } 14790 }; 14791 14792 parsePat = function (payload, pat) { 14793 pat.section_number = payload[7]; // eslint-disable-line camelcase 14794 14795 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 14796 // skip the PSI header and parse the first PMT entry 14797 14798 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; 14799 pat.pmtPid = self.pmtPid; 14800 }; 14801 /** 14802 * Parse out the relevant fields of a Program Map Table (PMT). 14803 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 14804 * packet. The first byte in this array should be the table_id 14805 * field. 14806 * @param pmt {object} the object that should be decorated with 14807 * fields parsed from the PMT. 14808 */ 14809 14810 14811 parsePmt = function (payload, pmt) { 14812 var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually 14813 // take effect. We don't believe this should ever be the case 14814 // for HLS but we'll ignore "forward" PMT declarations if we see 14815 // them. Future PMT declarations have the current_next_indicator 14816 // set to zero. 14817 14818 if (!(payload[5] & 0x01)) { 14819 return; 14820 } // overwrite any existing program map table 14821 14822 14823 self.programMapTable = { 14824 video: null, 14825 audio: null, 14826 'timed-metadata': {} 14827 }; // the mapping table ends at the end of the current section 14828 14829 sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; 14830 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 14831 // long the program info descriptors are 14832 14833 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table 14834 14835 offset = 12 + programInfoLength; 14836 14837 while (offset < tableEnd) { 14838 var streamType = payload[offset]; 14839 var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types 14840 // TODO: should this be done for metadata too? for now maintain behavior of 14841 // multiple metadata streams 14842 14843 if (streamType === StreamTypes$2.H264_STREAM_TYPE && self.programMapTable.video === null) { 14844 self.programMapTable.video = pid; 14845 } else if (streamType === StreamTypes$2.ADTS_STREAM_TYPE && self.programMapTable.audio === null) { 14846 self.programMapTable.audio = pid; 14847 } else if (streamType === StreamTypes$2.METADATA_STREAM_TYPE) { 14848 // map pid to stream type for metadata streams 14849 self.programMapTable['timed-metadata'][pid] = streamType; 14850 } // move to the next table entry 14851 // skip past the elementary stream descriptors, if present 14852 14853 14854 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; 14855 } // record the map on the packet as well 14856 14857 14858 pmt.programMapTable = self.programMapTable; 14859 }; 14860 /** 14861 * Deliver a new MP2T packet to the next stream in the pipeline. 14862 */ 14863 14864 14865 this.push = function (packet) { 14866 var result = {}, 14867 offset = 4; 14868 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1] 14869 14870 result.pid = packet[1] & 0x1f; 14871 result.pid <<= 8; 14872 result.pid |= packet[2]; // if an adaption field is present, its length is specified by the 14873 // fifth byte of the TS packet header. The adaptation field is 14874 // used to add stuffing to PES packets that don't fill a complete 14875 // TS packet, and to specify some forms of timing and control data 14876 // that we do not currently use. 14877 14878 if ((packet[3] & 0x30) >>> 4 > 0x01) { 14879 offset += packet[offset] + 1; 14880 } // parse the rest of the packet based on the type 14881 14882 14883 if (result.pid === 0) { 14884 result.type = 'pat'; 14885 parsePsi(packet.subarray(offset), result); 14886 this.trigger('data', result); 14887 } else if (result.pid === this.pmtPid) { 14888 result.type = 'pmt'; 14889 parsePsi(packet.subarray(offset), result); 14890 this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now 14891 14892 while (this.packetsWaitingForPmt.length) { 14893 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 14894 } 14895 } else if (this.programMapTable === undefined) { 14896 // When we have not seen a PMT yet, defer further processing of 14897 // PES packets until one has been parsed 14898 this.packetsWaitingForPmt.push([packet, offset, result]); 14899 } else { 14900 this.processPes_(packet, offset, result); 14901 } 14902 }; 14903 14904 this.processPes_ = function (packet, offset, result) { 14905 // set the appropriate stream type 14906 if (result.pid === this.programMapTable.video) { 14907 result.streamType = StreamTypes$2.H264_STREAM_TYPE; 14908 } else if (result.pid === this.programMapTable.audio) { 14909 result.streamType = StreamTypes$2.ADTS_STREAM_TYPE; 14910 } else { 14911 // if not video or audio, it is timed-metadata or unknown 14912 // if unknown, streamType will be undefined 14913 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 14914 } 14915 14916 result.type = 'pes'; 14917 result.data = packet.subarray(offset); 14918 this.trigger('data', result); 14919 }; 14920 }; 14921 14922 TransportParseStream.prototype = new Stream$4(); 14923 TransportParseStream.STREAM_TYPES = { 14924 h264: 0x1b, 14925 adts: 0x0f 14926 }; 14927 /** 14928 * Reconsistutes program elementary stream (PES) packets from parsed 14929 * transport stream packets. That is, if you pipe an 14930 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 14931 * events will be events which capture the bytes for individual PES 14932 * packets plus relevant metadata that has been extracted from the 14933 * container. 14934 */ 14935 14936 ElementaryStream = function () { 14937 var self = this, 14938 segmentHadPmt = false, 14939 // PES packet fragments 14940 video = { 14941 data: [], 14942 size: 0 14943 }, 14944 audio = { 14945 data: [], 14946 size: 0 14947 }, 14948 timedMetadata = { 14949 data: [], 14950 size: 0 14951 }, 14952 programMapTable, 14953 parsePes = function (payload, pes) { 14954 var ptsDtsFlags; 14955 const startPrefix = payload[0] << 16 | payload[1] << 8 | payload[2]; // default to an empty array 14956 14957 pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets 14958 // that are frame data that is continuing from the previous fragment. This 14959 // is to check that the pes data is the start of a new pes payload 14960 14961 if (startPrefix !== 1) { 14962 return; 14963 } // get the packet length, this will be 0 for video 14964 14965 14966 pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe 14967 14968 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value 14969 // and a DTS value. Determine what combination of values is 14970 // available to work with. 14971 14972 ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 14973 // performs all bitwise operations on 32-bit integers but javascript 14974 // supports a much greater range (52-bits) of integer using standard 14975 // mathematical operations. 14976 // We construct a 31-bit value using bitwise operators over the 31 14977 // most significant bits and then multiply by 4 (equal to a left-shift 14978 // of 2) before we add the final 2 least significant bits of the 14979 // timestamp (equal to an OR.) 14980 14981 if (ptsDtsFlags & 0xC0) { 14982 // the PTS and DTS are not written out directly. For information 14983 // on how they are encoded, see 14984 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 14985 pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3; 14986 pes.pts *= 4; // Left shift by 2 14987 14988 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 14989 14990 pes.dts = pes.pts; 14991 14992 if (ptsDtsFlags & 0x40) { 14993 pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3; 14994 pes.dts *= 4; // Left shift by 2 14995 14996 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 14997 } 14998 } // the data section starts immediately after the PES header. 14999 // pes_header_data_length specifies the number of header bytes 15000 // that follow the last byte of the field. 15001 15002 15003 pes.data = payload.subarray(9 + payload[8]); 15004 }, 15005 15006 /** 15007 * Pass completely parsed PES packets to the next stream in the pipeline 15008 **/ 15009 flushStream = function (stream, type, forceFlush) { 15010 var packetData = new Uint8Array(stream.size), 15011 event = { 15012 type: type 15013 }, 15014 i = 0, 15015 offset = 0, 15016 packetFlushable = false, 15017 fragment; // do nothing if there is not enough buffered data for a complete 15018 // PES header 15019 15020 if (!stream.data.length || stream.size < 9) { 15021 return; 15022 } 15023 15024 event.trackId = stream.data[0].pid; // reassemble the packet 15025 15026 for (i = 0; i < stream.data.length; i++) { 15027 fragment = stream.data[i]; 15028 packetData.set(fragment.data, offset); 15029 offset += fragment.data.byteLength; 15030 } // parse assembled packet's PES header 15031 15032 15033 parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length 15034 // check that there is enough stream data to fill the packet 15035 15036 packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right 15037 15038 if (forceFlush || packetFlushable) { 15039 stream.size = 0; 15040 stream.data.length = 0; 15041 } // only emit packets that are complete. this is to avoid assembling 15042 // incomplete PES packets due to poor segmentation 15043 15044 15045 if (packetFlushable) { 15046 self.trigger('data', event); 15047 } 15048 }; 15049 15050 ElementaryStream.prototype.init.call(this); 15051 /** 15052 * Identifies M2TS packet types and parses PES packets using metadata 15053 * parsed from the PMT 15054 **/ 15055 15056 this.push = function (data) { 15057 ({ 15058 pat: function () {// we have to wait for the PMT to arrive as well before we 15059 // have any meaningful metadata 15060 }, 15061 pes: function () { 15062 var stream, streamType; 15063 15064 switch (data.streamType) { 15065 case StreamTypes$2.H264_STREAM_TYPE: 15066 stream = video; 15067 streamType = 'video'; 15068 break; 15069 15070 case StreamTypes$2.ADTS_STREAM_TYPE: 15071 stream = audio; 15072 streamType = 'audio'; 15073 break; 15074 15075 case StreamTypes$2.METADATA_STREAM_TYPE: 15076 stream = timedMetadata; 15077 streamType = 'timed-metadata'; 15078 break; 15079 15080 default: 15081 // ignore unknown stream types 15082 return; 15083 } // if a new packet is starting, we can flush the completed 15084 // packet 15085 15086 15087 if (data.payloadUnitStartIndicator) { 15088 flushStream(stream, streamType, true); 15089 } // buffer this fragment until we are sure we've received the 15090 // complete payload 15091 15092 15093 stream.data.push(data); 15094 stream.size += data.data.byteLength; 15095 }, 15096 pmt: function () { 15097 var event = { 15098 type: 'metadata', 15099 tracks: [] 15100 }; 15101 programMapTable = data.programMapTable; // translate audio and video streams to tracks 15102 15103 if (programMapTable.video !== null) { 15104 event.tracks.push({ 15105 timelineStartInfo: { 15106 baseMediaDecodeTime: 0 15107 }, 15108 id: +programMapTable.video, 15109 codec: 'avc', 15110 type: 'video' 15111 }); 15112 } 15113 15114 if (programMapTable.audio !== null) { 15115 event.tracks.push({ 15116 timelineStartInfo: { 15117 baseMediaDecodeTime: 0 15118 }, 15119 id: +programMapTable.audio, 15120 codec: 'adts', 15121 type: 'audio' 15122 }); 15123 } 15124 15125 segmentHadPmt = true; 15126 self.trigger('data', event); 15127 } 15128 })[data.type](); 15129 }; 15130 15131 this.reset = function () { 15132 video.size = 0; 15133 video.data.length = 0; 15134 audio.size = 0; 15135 audio.data.length = 0; 15136 this.trigger('reset'); 15137 }; 15138 /** 15139 * Flush any remaining input. Video PES packets may be of variable 15140 * length. Normally, the start of a new video packet can trigger the 15141 * finalization of the previous packet. That is not possible if no 15142 * more video is forthcoming, however. In that case, some other 15143 * mechanism (like the end of the file) has to be employed. When it is 15144 * clear that no additional data is forthcoming, calling this method 15145 * will flush the buffered packets. 15146 */ 15147 15148 15149 this.flushStreams_ = function () { 15150 // !!THIS ORDER IS IMPORTANT!! 15151 // video first then audio 15152 flushStream(video, 'video'); 15153 flushStream(audio, 'audio'); 15154 flushStream(timedMetadata, 'timed-metadata'); 15155 }; 15156 15157 this.flush = function () { 15158 // if on flush we haven't had a pmt emitted 15159 // and we have a pmt to emit. emit the pmt 15160 // so that we trigger a trackinfo downstream. 15161 if (!segmentHadPmt && programMapTable) { 15162 var pmt = { 15163 type: 'metadata', 15164 tracks: [] 15165 }; // translate audio and video streams to tracks 15166 15167 if (programMapTable.video !== null) { 15168 pmt.tracks.push({ 15169 timelineStartInfo: { 15170 baseMediaDecodeTime: 0 15171 }, 15172 id: +programMapTable.video, 15173 codec: 'avc', 15174 type: 'video' 15175 }); 15176 } 15177 15178 if (programMapTable.audio !== null) { 15179 pmt.tracks.push({ 15180 timelineStartInfo: { 15181 baseMediaDecodeTime: 0 15182 }, 15183 id: +programMapTable.audio, 15184 codec: 'adts', 15185 type: 'audio' 15186 }); 15187 } 15188 15189 self.trigger('data', pmt); 15190 } 15191 15192 segmentHadPmt = false; 15193 this.flushStreams_(); 15194 this.trigger('done'); 15195 }; 15196 }; 15197 15198 ElementaryStream.prototype = new Stream$4(); 15199 var m2ts$1 = { 15200 PAT_PID: 0x0000, 15201 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH$1, 15202 TransportPacketStream: TransportPacketStream, 15203 TransportParseStream: TransportParseStream, 15204 ElementaryStream: ElementaryStream, 15205 TimestampRolloverStream: TimestampRolloverStream, 15206 CaptionStream: CaptionStream$1.CaptionStream, 15207 Cea608Stream: CaptionStream$1.Cea608Stream, 15208 Cea708Stream: CaptionStream$1.Cea708Stream, 15209 MetadataStream: metadataStream 15210 }; 15211 15212 for (var type in StreamTypes$2) { 15213 if (StreamTypes$2.hasOwnProperty(type)) { 15214 m2ts$1[type] = StreamTypes$2[type]; 15215 } 15216 } 15217 15218 var m2ts_1 = m2ts$1; 15219 /** 15220 * mux.js 15221 * 15222 * Copyright (c) Brightcove 15223 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15224 */ 15225 15226 var Stream$3 = stream; 15227 var ONE_SECOND_IN_TS$2 = clock$2.ONE_SECOND_IN_TS; 15228 var AdtsStream$1; 15229 var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 15230 /* 15231 * Accepts a ElementaryStream and emits data events with parsed 15232 * AAC Audio Frames of the individual packets. Input audio in ADTS 15233 * format is unpacked and re-emitted as AAC frames. 15234 * 15235 * @see http://wiki.multimedia.cx/index.php?title=ADTS 15236 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 15237 */ 15238 15239 AdtsStream$1 = function (handlePartialSegments) { 15240 var buffer, 15241 frameNum = 0; 15242 AdtsStream$1.prototype.init.call(this); 15243 15244 this.skipWarn_ = function (start, end) { 15245 this.trigger('log', { 15246 level: 'warn', 15247 message: `adts skiping bytes ${start} to ${end} in frame ${frameNum} outside syncword` 15248 }); 15249 }; 15250 15251 this.push = function (packet) { 15252 var i = 0, 15253 frameLength, 15254 protectionSkipBytes, 15255 oldBuffer, 15256 sampleCount, 15257 adtsFrameDuration; 15258 15259 if (!handlePartialSegments) { 15260 frameNum = 0; 15261 } 15262 15263 if (packet.type !== 'audio') { 15264 // ignore non-audio data 15265 return; 15266 } // Prepend any data in the buffer to the input data so that we can parse 15267 // aac frames the cross a PES packet boundary 15268 15269 15270 if (buffer && buffer.length) { 15271 oldBuffer = buffer; 15272 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 15273 buffer.set(oldBuffer); 15274 buffer.set(packet.data, oldBuffer.byteLength); 15275 } else { 15276 buffer = packet.data; 15277 } // unpack any ADTS frames which have been fully received 15278 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 15279 15280 15281 var skip; // We use i + 7 here because we want to be able to parse the entire header. 15282 // If we don't have enough bytes to do that, then we definitely won't have a full frame. 15283 15284 while (i + 7 < buffer.length) { 15285 // Look for the start of an ADTS header.. 15286 if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) { 15287 if (typeof skip !== 'number') { 15288 skip = i; 15289 } // If a valid header was not found, jump one forward and attempt to 15290 // find a valid ADTS header starting at the next byte 15291 15292 15293 i++; 15294 continue; 15295 } 15296 15297 if (typeof skip === 'number') { 15298 this.skipWarn_(skip, i); 15299 skip = null; 15300 } // The protection skip bit tells us if we have 2 bytes of CRC data at the 15301 // end of the ADTS header 15302 15303 15304 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the 15305 // end of the sync sequence 15306 // NOTE: frame length includes the size of the header 15307 15308 frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5; 15309 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 15310 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, 15311 // then we have to wait for more data 15312 15313 if (buffer.byteLength - i < frameLength) { 15314 break; 15315 } // Otherwise, deliver the complete AAC frame 15316 15317 15318 this.trigger('data', { 15319 pts: packet.pts + frameNum * adtsFrameDuration, 15320 dts: packet.dts + frameNum * adtsFrameDuration, 15321 sampleCount: sampleCount, 15322 audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1, 15323 channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6, 15324 samplerate: ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2], 15325 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 15326 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 15327 samplesize: 16, 15328 // data is the frame without it's header 15329 data: buffer.subarray(i + 7 + protectionSkipBytes, i + frameLength) 15330 }); 15331 frameNum++; 15332 i += frameLength; 15333 } 15334 15335 if (typeof skip === 'number') { 15336 this.skipWarn_(skip, i); 15337 skip = null; 15338 } // remove processed bytes from the buffer. 15339 15340 15341 buffer = buffer.subarray(i); 15342 }; 15343 15344 this.flush = function () { 15345 frameNum = 0; 15346 this.trigger('done'); 15347 }; 15348 15349 this.reset = function () { 15350 buffer = void 0; 15351 this.trigger('reset'); 15352 }; 15353 15354 this.endTimeline = function () { 15355 buffer = void 0; 15356 this.trigger('endedtimeline'); 15357 }; 15358 }; 15359 15360 AdtsStream$1.prototype = new Stream$3(); 15361 var adts = AdtsStream$1; 15362 /** 15363 * mux.js 15364 * 15365 * Copyright (c) Brightcove 15366 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15367 */ 15368 15369 var ExpGolomb$1; 15370 /** 15371 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 15372 * scheme used by h264. 15373 */ 15374 15375 ExpGolomb$1 = function (workingData) { 15376 var // the number of bytes left to examine in workingData 15377 workingBytesAvailable = workingData.byteLength, 15378 // the current word being examined 15379 workingWord = 0, 15380 // :uint 15381 // the number of bits left to examine in the current word 15382 workingBitsAvailable = 0; // :uint; 15383 // ():uint 15384 15385 this.length = function () { 15386 return 8 * workingBytesAvailable; 15387 }; // ():uint 15388 15389 15390 this.bitsAvailable = function () { 15391 return 8 * workingBytesAvailable + workingBitsAvailable; 15392 }; // ():void 15393 15394 15395 this.loadWord = function () { 15396 var position = workingData.byteLength - workingBytesAvailable, 15397 workingBytes = new Uint8Array(4), 15398 availableBytes = Math.min(4, workingBytesAvailable); 15399 15400 if (availableBytes === 0) { 15401 throw new Error('no bytes available'); 15402 } 15403 15404 workingBytes.set(workingData.subarray(position, position + availableBytes)); 15405 workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed 15406 15407 workingBitsAvailable = availableBytes * 8; 15408 workingBytesAvailable -= availableBytes; 15409 }; // (count:int):void 15410 15411 15412 this.skipBits = function (count) { 15413 var skipBytes; // :int 15414 15415 if (workingBitsAvailable > count) { 15416 workingWord <<= count; 15417 workingBitsAvailable -= count; 15418 } else { 15419 count -= workingBitsAvailable; 15420 skipBytes = Math.floor(count / 8); 15421 count -= skipBytes * 8; 15422 workingBytesAvailable -= skipBytes; 15423 this.loadWord(); 15424 workingWord <<= count; 15425 workingBitsAvailable -= count; 15426 } 15427 }; // (size:int):uint 15428 15429 15430 this.readBits = function (size) { 15431 var bits = Math.min(workingBitsAvailable, size), 15432 // :uint 15433 valu = workingWord >>> 32 - bits; // :uint 15434 // if size > 31, handle error 15435 15436 workingBitsAvailable -= bits; 15437 15438 if (workingBitsAvailable > 0) { 15439 workingWord <<= bits; 15440 } else if (workingBytesAvailable > 0) { 15441 this.loadWord(); 15442 } 15443 15444 bits = size - bits; 15445 15446 if (bits > 0) { 15447 return valu << bits | this.readBits(bits); 15448 } 15449 15450 return valu; 15451 }; // ():uint 15452 15453 15454 this.skipLeadingZeros = function () { 15455 var leadingZeroCount; // :uint 15456 15457 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) { 15458 if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) { 15459 // the first bit of working word is 1 15460 workingWord <<= leadingZeroCount; 15461 workingBitsAvailable -= leadingZeroCount; 15462 return leadingZeroCount; 15463 } 15464 } // we exhausted workingWord and still have not found a 1 15465 15466 15467 this.loadWord(); 15468 return leadingZeroCount + this.skipLeadingZeros(); 15469 }; // ():void 15470 15471 15472 this.skipUnsignedExpGolomb = function () { 15473 this.skipBits(1 + this.skipLeadingZeros()); 15474 }; // ():void 15475 15476 15477 this.skipExpGolomb = function () { 15478 this.skipBits(1 + this.skipLeadingZeros()); 15479 }; // ():uint 15480 15481 15482 this.readUnsignedExpGolomb = function () { 15483 var clz = this.skipLeadingZeros(); // :uint 15484 15485 return this.readBits(clz + 1) - 1; 15486 }; // ():int 15487 15488 15489 this.readExpGolomb = function () { 15490 var valu = this.readUnsignedExpGolomb(); // :int 15491 15492 if (0x01 & valu) { 15493 // the number is odd if the low order bit is set 15494 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 15495 } 15496 15497 return -1 * (valu >>> 1); // divide by two then make it negative 15498 }; // Some convenience functions 15499 // :Boolean 15500 15501 15502 this.readBoolean = function () { 15503 return this.readBits(1) === 1; 15504 }; // ():int 15505 15506 15507 this.readUnsignedByte = function () { 15508 return this.readBits(8); 15509 }; 15510 15511 this.loadWord(); 15512 }; 15513 15514 var expGolomb = ExpGolomb$1; 15515 /** 15516 * mux.js 15517 * 15518 * Copyright (c) Brightcove 15519 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 15520 */ 15521 15522 var Stream$2 = stream; 15523 var ExpGolomb = expGolomb; 15524 var H264Stream$1, NalByteStream; 15525 var PROFILES_WITH_OPTIONAL_SPS_DATA; 15526 /** 15527 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 15528 */ 15529 15530 NalByteStream = function () { 15531 var syncPoint = 0, 15532 i, 15533 buffer; 15534 NalByteStream.prototype.init.call(this); 15535 /* 15536 * Scans a byte stream and triggers a data event with the NAL units found. 15537 * @param {Object} data Event received from H264Stream 15538 * @param {Uint8Array} data.data The h264 byte stream to be scanned 15539 * 15540 * @see H264Stream.push 15541 */ 15542 15543 this.push = function (data) { 15544 var swapBuffer; 15545 15546 if (!buffer) { 15547 buffer = data.data; 15548 } else { 15549 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 15550 swapBuffer.set(buffer); 15551 swapBuffer.set(data.data, buffer.byteLength); 15552 buffer = swapBuffer; 15553 } 15554 15555 var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B 15556 // scan for NAL unit boundaries 15557 // a match looks like this: 15558 // 0 0 1 .. NAL .. 0 0 1 15559 // ^ sync point ^ i 15560 // or this: 15561 // 0 0 1 .. NAL .. 0 0 0 15562 // ^ sync point ^ i 15563 // advance the sync point to a NAL start, if necessary 15564 15565 for (; syncPoint < len - 3; syncPoint++) { 15566 if (buffer[syncPoint + 2] === 1) { 15567 // the sync point is properly aligned 15568 i = syncPoint + 5; 15569 break; 15570 } 15571 } 15572 15573 while (i < len) { 15574 // look at the current byte to determine if we've hit the end of 15575 // a NAL unit boundary 15576 switch (buffer[i]) { 15577 case 0: 15578 // skip past non-sync sequences 15579 if (buffer[i - 1] !== 0) { 15580 i += 2; 15581 break; 15582 } else if (buffer[i - 2] !== 0) { 15583 i++; 15584 break; 15585 } // deliver the NAL unit if it isn't empty 15586 15587 15588 if (syncPoint + 3 !== i - 2) { 15589 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 15590 } // drop trailing zeroes 15591 15592 15593 do { 15594 i++; 15595 } while (buffer[i] !== 1 && i < len); 15596 15597 syncPoint = i - 2; 15598 i += 3; 15599 break; 15600 15601 case 1: 15602 // skip past non-sync sequences 15603 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 15604 i += 3; 15605 break; 15606 } // deliver the NAL unit 15607 15608 15609 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 15610 syncPoint = i - 2; 15611 i += 3; 15612 break; 15613 15614 default: 15615 // the current byte isn't a one or zero, so it cannot be part 15616 // of a sync sequence 15617 i += 3; 15618 break; 15619 } 15620 } // filter out the NAL units that were delivered 15621 15622 15623 buffer = buffer.subarray(syncPoint); 15624 i -= syncPoint; 15625 syncPoint = 0; 15626 }; 15627 15628 this.reset = function () { 15629 buffer = null; 15630 syncPoint = 0; 15631 this.trigger('reset'); 15632 }; 15633 15634 this.flush = function () { 15635 // deliver the last buffered NAL unit 15636 if (buffer && buffer.byteLength > 3) { 15637 this.trigger('data', buffer.subarray(syncPoint + 3)); 15638 } // reset the stream state 15639 15640 15641 buffer = null; 15642 syncPoint = 0; 15643 this.trigger('done'); 15644 }; 15645 15646 this.endTimeline = function () { 15647 this.flush(); 15648 this.trigger('endedtimeline'); 15649 }; 15650 }; 15651 15652 NalByteStream.prototype = new Stream$2(); // values of profile_idc that indicate additional fields are included in the SPS 15653 // see Recommendation ITU-T H.264 (4/2013), 15654 // 7.3.2.1.1 Sequence parameter set data syntax 15655 15656 PROFILES_WITH_OPTIONAL_SPS_DATA = { 15657 100: true, 15658 110: true, 15659 122: true, 15660 244: true, 15661 44: true, 15662 83: true, 15663 86: true, 15664 118: true, 15665 128: true, 15666 // TODO: the three profiles below don't 15667 // appear to have sps data in the specificiation anymore? 15668 138: true, 15669 139: true, 15670 134: true 15671 }; 15672 /** 15673 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 15674 * events. 15675 */ 15676 15677 H264Stream$1 = function () { 15678 var nalByteStream = new NalByteStream(), 15679 self, 15680 trackId, 15681 currentPts, 15682 currentDts, 15683 discardEmulationPreventionBytes, 15684 readSequenceParameterSet, 15685 skipScalingList; 15686 H264Stream$1.prototype.init.call(this); 15687 self = this; 15688 /* 15689 * Pushes a packet from a stream onto the NalByteStream 15690 * 15691 * @param {Object} packet - A packet received from a stream 15692 * @param {Uint8Array} packet.data - The raw bytes of the packet 15693 * @param {Number} packet.dts - Decode timestamp of the packet 15694 * @param {Number} packet.pts - Presentation timestamp of the packet 15695 * @param {Number} packet.trackId - The id of the h264 track this packet came from 15696 * @param {('video'|'audio')} packet.type - The type of packet 15697 * 15698 */ 15699 15700 this.push = function (packet) { 15701 if (packet.type !== 'video') { 15702 return; 15703 } 15704 15705 trackId = packet.trackId; 15706 currentPts = packet.pts; 15707 currentDts = packet.dts; 15708 nalByteStream.push(packet); 15709 }; 15710 /* 15711 * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps 15712 * for the NALUs to the next stream component. 15713 * Also, preprocess caption and sequence parameter NALUs. 15714 * 15715 * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push` 15716 * @see NalByteStream.push 15717 */ 15718 15719 15720 nalByteStream.on('data', function (data) { 15721 var event = { 15722 trackId: trackId, 15723 pts: currentPts, 15724 dts: currentDts, 15725 data: data, 15726 nalUnitTypeCode: data[0] & 0x1f 15727 }; 15728 15729 switch (event.nalUnitTypeCode) { 15730 case 0x05: 15731 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 15732 break; 15733 15734 case 0x06: 15735 event.nalUnitType = 'sei_rbsp'; 15736 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 15737 break; 15738 15739 case 0x07: 15740 event.nalUnitType = 'seq_parameter_set_rbsp'; 15741 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 15742 event.config = readSequenceParameterSet(event.escapedRBSP); 15743 break; 15744 15745 case 0x08: 15746 event.nalUnitType = 'pic_parameter_set_rbsp'; 15747 break; 15748 15749 case 0x09: 15750 event.nalUnitType = 'access_unit_delimiter_rbsp'; 15751 break; 15752 } // This triggers data on the H264Stream 15753 15754 15755 self.trigger('data', event); 15756 }); 15757 nalByteStream.on('done', function () { 15758 self.trigger('done'); 15759 }); 15760 nalByteStream.on('partialdone', function () { 15761 self.trigger('partialdone'); 15762 }); 15763 nalByteStream.on('reset', function () { 15764 self.trigger('reset'); 15765 }); 15766 nalByteStream.on('endedtimeline', function () { 15767 self.trigger('endedtimeline'); 15768 }); 15769 15770 this.flush = function () { 15771 nalByteStream.flush(); 15772 }; 15773 15774 this.partialFlush = function () { 15775 nalByteStream.partialFlush(); 15776 }; 15777 15778 this.reset = function () { 15779 nalByteStream.reset(); 15780 }; 15781 15782 this.endTimeline = function () { 15783 nalByteStream.endTimeline(); 15784 }; 15785 /** 15786 * Advance the ExpGolomb decoder past a scaling list. The scaling 15787 * list is optionally transmitted as part of a sequence parameter 15788 * set and is not relevant to transmuxing. 15789 * @param count {number} the number of entries in this scaling list 15790 * @param expGolombDecoder {object} an ExpGolomb pointed to the 15791 * start of a scaling list 15792 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 15793 */ 15794 15795 15796 skipScalingList = function (count, expGolombDecoder) { 15797 var lastScale = 8, 15798 nextScale = 8, 15799 j, 15800 deltaScale; 15801 15802 for (j = 0; j < count; j++) { 15803 if (nextScale !== 0) { 15804 deltaScale = expGolombDecoder.readExpGolomb(); 15805 nextScale = (lastScale + deltaScale + 256) % 256; 15806 } 15807 15808 lastScale = nextScale === 0 ? lastScale : nextScale; 15809 } 15810 }; 15811 /** 15812 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 15813 * Sequence Payload" 15814 * @param data {Uint8Array} the bytes of a RBSP from a NAL 15815 * unit 15816 * @return {Uint8Array} the RBSP without any Emulation 15817 * Prevention Bytes 15818 */ 15819 15820 15821 discardEmulationPreventionBytes = function (data) { 15822 var length = data.byteLength, 15823 emulationPreventionBytesPositions = [], 15824 i = 1, 15825 newLength, 15826 newData; // Find all `Emulation Prevention Bytes` 15827 15828 while (i < length - 2) { 15829 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 15830 emulationPreventionBytesPositions.push(i + 2); 15831 i += 2; 15832 } else { 15833 i++; 15834 } 15835 } // If no Emulation Prevention Bytes were found just return the original 15836 // array 15837 15838 15839 if (emulationPreventionBytesPositions.length === 0) { 15840 return data; 15841 } // Create a new array to hold the NAL unit data 15842 15843 15844 newLength = length - emulationPreventionBytesPositions.length; 15845 newData = new Uint8Array(newLength); 15846 var sourceIndex = 0; 15847 15848 for (i = 0; i < newLength; sourceIndex++, i++) { 15849 if (sourceIndex === emulationPreventionBytesPositions[0]) { 15850 // Skip this byte 15851 sourceIndex++; // Remove this position index 15852 15853 emulationPreventionBytesPositions.shift(); 15854 } 15855 15856 newData[i] = data[sourceIndex]; 15857 } 15858 15859 return newData; 15860 }; 15861 /** 15862 * Read a sequence parameter set and return some interesting video 15863 * properties. A sequence parameter set is the H264 metadata that 15864 * describes the properties of upcoming video frames. 15865 * @param data {Uint8Array} the bytes of a sequence parameter set 15866 * @return {object} an object with configuration parsed from the 15867 * sequence parameter set, including the dimensions of the 15868 * associated video frames. 15869 */ 15870 15871 15872 readSequenceParameterSet = function (data) { 15873 var frameCropLeftOffset = 0, 15874 frameCropRightOffset = 0, 15875 frameCropTopOffset = 0, 15876 frameCropBottomOffset = 0, 15877 expGolombDecoder, 15878 profileIdc, 15879 levelIdc, 15880 profileCompatibility, 15881 chromaFormatIdc, 15882 picOrderCntType, 15883 numRefFramesInPicOrderCntCycle, 15884 picWidthInMbsMinus1, 15885 picHeightInMapUnitsMinus1, 15886 frameMbsOnlyFlag, 15887 scalingListCount, 15888 sarRatio = [1, 1], 15889 aspectRatioIdc, 15890 i; 15891 expGolombDecoder = new ExpGolomb(data); 15892 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 15893 15894 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 15895 15896 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 15897 15898 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 15899 // some profiles have more optional data we don't need 15900 15901 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 15902 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 15903 15904 if (chromaFormatIdc === 3) { 15905 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 15906 } 15907 15908 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 15909 15910 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 15911 15912 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 15913 15914 if (expGolombDecoder.readBoolean()) { 15915 // seq_scaling_matrix_present_flag 15916 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 15917 15918 for (i = 0; i < scalingListCount; i++) { 15919 if (expGolombDecoder.readBoolean()) { 15920 // seq_scaling_list_present_flag[ i ] 15921 if (i < 6) { 15922 skipScalingList(16, expGolombDecoder); 15923 } else { 15924 skipScalingList(64, expGolombDecoder); 15925 } 15926 } 15927 } 15928 } 15929 } 15930 15931 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 15932 15933 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 15934 15935 if (picOrderCntType === 0) { 15936 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 15937 } else if (picOrderCntType === 1) { 15938 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 15939 15940 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 15941 15942 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 15943 15944 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 15945 15946 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 15947 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 15948 } 15949 } 15950 15951 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 15952 15953 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 15954 15955 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 15956 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 15957 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 15958 15959 if (frameMbsOnlyFlag === 0) { 15960 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 15961 } 15962 15963 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 15964 15965 if (expGolombDecoder.readBoolean()) { 15966 // frame_cropping_flag 15967 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 15968 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 15969 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 15970 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 15971 } 15972 15973 if (expGolombDecoder.readBoolean()) { 15974 // vui_parameters_present_flag 15975 if (expGolombDecoder.readBoolean()) { 15976 // aspect_ratio_info_present_flag 15977 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 15978 15979 switch (aspectRatioIdc) { 15980 case 1: 15981 sarRatio = [1, 1]; 15982 break; 15983 15984 case 2: 15985 sarRatio = [12, 11]; 15986 break; 15987 15988 case 3: 15989 sarRatio = [10, 11]; 15990 break; 15991 15992 case 4: 15993 sarRatio = [16, 11]; 15994 break; 15995 15996 case 5: 15997 sarRatio = [40, 33]; 15998 break; 15999 16000 case 6: 16001 sarRatio = [24, 11]; 16002 break; 16003 16004 case 7: 16005 sarRatio = [20, 11]; 16006 break; 16007 16008 case 8: 16009 sarRatio = [32, 11]; 16010 break; 16011 16012 case 9: 16013 sarRatio = [80, 33]; 16014 break; 16015 16016 case 10: 16017 sarRatio = [18, 11]; 16018 break; 16019 16020 case 11: 16021 sarRatio = [15, 11]; 16022 break; 16023 16024 case 12: 16025 sarRatio = [64, 33]; 16026 break; 16027 16028 case 13: 16029 sarRatio = [160, 99]; 16030 break; 16031 16032 case 14: 16033 sarRatio = [4, 3]; 16034 break; 16035 16036 case 15: 16037 sarRatio = [3, 2]; 16038 break; 16039 16040 case 16: 16041 sarRatio = [2, 1]; 16042 break; 16043 16044 case 255: 16045 { 16046 sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()]; 16047 break; 16048 } 16049 } 16050 16051 if (sarRatio) { 16052 sarRatio[0] / sarRatio[1]; 16053 } 16054 } 16055 } 16056 16057 return { 16058 profileIdc: profileIdc, 16059 levelIdc: levelIdc, 16060 profileCompatibility: profileCompatibility, 16061 width: (picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2, 16062 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2, 16063 // sar is sample aspect ratio 16064 sarRatio: sarRatio 16065 }; 16066 }; 16067 }; 16068 16069 H264Stream$1.prototype = new Stream$2(); 16070 var h264 = { 16071 H264Stream: H264Stream$1, 16072 NalByteStream: NalByteStream 16073 }; 16074 /** 16075 * mux.js 16076 * 16077 * Copyright (c) Brightcove 16078 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 16079 * 16080 * Utilities to detect basic properties and metadata about Aac data. 16081 */ 16082 16083 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 16084 16085 var parseId3TagSize = function (header, byteIndex) { 16086 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 16087 flags = header[byteIndex + 5], 16088 footerPresent = (flags & 16) >> 4; // if we get a negative returnSize clamp it to 0 16089 16090 returnSize = returnSize >= 0 ? returnSize : 0; 16091 16092 if (footerPresent) { 16093 return returnSize + 20; 16094 } 16095 16096 return returnSize + 10; 16097 }; 16098 16099 var getId3Offset = function (data, offset) { 16100 if (data.length - offset < 10 || data[offset] !== 'I'.charCodeAt(0) || data[offset + 1] !== 'D'.charCodeAt(0) || data[offset + 2] !== '3'.charCodeAt(0)) { 16101 return offset; 16102 } 16103 16104 offset += parseId3TagSize(data, offset); 16105 return getId3Offset(data, offset); 16106 }; // TODO: use vhs-utils 16107 16108 16109 var isLikelyAacData$1 = function (data) { 16110 var offset = getId3Offset(data, 0); 16111 return data.length >= offset + 2 && (data[offset] & 0xFF) === 0xFF && (data[offset + 1] & 0xF0) === 0xF0 && // verify that the 2 layer bits are 0, aka this 16112 // is not mp3 data but aac data. 16113 (data[offset + 1] & 0x16) === 0x10; 16114 }; 16115 16116 var parseSyncSafeInteger = function (data) { 16117 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 16118 }; // return a percent-encoded representation of the specified byte range 16119 // @see http://en.wikipedia.org/wiki/Percent-encoding 16120 16121 16122 var percentEncode = function (bytes, start, end) { 16123 var i, 16124 result = ''; 16125 16126 for (i = start; i < end; i++) { 16127 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 16128 } 16129 16130 return result; 16131 }; // return the string representation of the specified byte range, 16132 // interpreted as ISO-8859-1. 16133 16134 16135 var parseIso88591 = function (bytes, start, end) { 16136 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 16137 }; 16138 16139 var parseAdtsSize = function (header, byteIndex) { 16140 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 16141 middle = header[byteIndex + 4] << 3, 16142 highTwo = header[byteIndex + 3] & 0x3 << 11; 16143 return highTwo | middle | lowThree; 16144 }; 16145 16146 var parseType$
vendor: 2,706 bytes, lines 16146-16229
161464 = function (header, byteIndex) { 16147 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 16148 return 'timed-metadata'; 16149 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 16150 return 'audio'; 16151 } 16152 16153 return null; 16154 }; 16155 16156 var parseSampleRate = function (packet) { 16157 var i = 0; 16158 16159 while (i + 5 < packet.length) { 16160 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 16161 // If a valid header was not found, jump one forward and attempt to 16162 // find a valid ADTS header starting at the next byte 16163 i++; 16164 continue; 16165 } 16166 16167 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 16168 } 16169 16170 return null; 16171 }; 16172 16173 var parseAacTimestamp = function (packet) { 16174 var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag 16175 16176 frameStart = 10; 16177 16178 if (packet[5] & 0x40) { 16179 // advance the frame start past the extended header 16180 frameStart += 4; // header size field 16181 16182 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 16183 } // parse one or more ID3 frames 16184 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 16185 16186 16187 do { 16188 // determine the number of bytes in this frame 16189 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 16190 16191 if (frameSize < 1) { 16192 return null; 16193 } 16194 16195 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 16196 16197 if (frameHeader === 'PRIV') { 16198 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 16199 16200 for (var i = 0; i < frame.byteLength; i++) { 16201 if (frame[i] === 0) { 16202 var owner = parseIso88591(frame, 0, i); 16203 16204 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 16205 var d = frame.subarray(i + 1); 16206 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 16207 size *= 4; 16208 size += d[7] & 0x03; 16209 return size; 16210 } 16211 16212 break; 16213 } 16214 } 16215 } 16216 16217 frameStart += 10; // advance past the frame header 16218 16219 frameStart += frameSize; // advance past the frame body 16220 } while (frameStart < packet.byteLength); 16221 16222 return null; 16223 }; 16224 16225 var utils = { 16226 isLikelyAacData: isLikelyAacData$1, 16227 parseId3TagSize: parseId3TagSize, 16228 parseAdtsSize: parseAdtsSize, 16229 parseType: parseType$4
vendor: 47,351 bytes, lines 16229-17467
16229, 16230 parseSampleRate: parseSampleRate, 16231 parseAacTimestamp: parseAacTimestamp 16232 }; 16233 /** 16234 * mux.js 16235 * 16236 * Copyright (c) Brightcove 16237 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 16238 * 16239 * A stream-based aac to mp4 converter. This utility can be used to 16240 * deliver mp4s to a SourceBuffer on platforms that support native 16241 * Media Source Extensions. 16242 */ 16243 16244 var Stream$1 = stream; 16245 var aacUtils = utils; // Constants 16246 16247 var AacStream$1; 16248 /** 16249 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 16250 */ 16251 16252 AacStream$1 = function () { 16253 var everything = new Uint8Array(), 16254 timeStamp = 0; 16255 AacStream$1.prototype.init.call(this); 16256 16257 this.setTimestamp = function (timestamp) { 16258 timeStamp = timestamp; 16259 }; 16260 16261 this.push = function (bytes) { 16262 var frameSize = 0, 16263 byteIndex = 0, 16264 bytesLeft, 16265 chunk, 16266 packet, 16267 tempLength; // If there are bytes remaining from the last segment, prepend them to the 16268 // bytes that were pushed in 16269 16270 if (everything.length) { 16271 tempLength = everything.length; 16272 everything = new Uint8Array(bytes.byteLength + tempLength); 16273 everything.set(everything.subarray(0, tempLength)); 16274 everything.set(bytes, tempLength); 16275 } else { 16276 everything = bytes; 16277 } 16278 16279 while (everything.length - byteIndex >= 3) { 16280 if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) { 16281 // Exit early because we don't have enough to parse 16282 // the ID3 tag header 16283 if (everything.length - byteIndex < 10) { 16284 break; 16285 } // check framesize 16286 16287 16288 frameSize = aacUtils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 16289 // to emit a full packet 16290 // Add to byteIndex to support multiple ID3 tags in sequence 16291 16292 if (byteIndex + frameSize > everything.length) { 16293 break; 16294 } 16295 16296 chunk = { 16297 type: 'timed-metadata', 16298 data: everything.subarray(byteIndex, byteIndex + frameSize) 16299 }; 16300 this.trigger('data', chunk); 16301 byteIndex += frameSize; 16302 continue; 16303 } else if ((everything[byteIndex] & 0xff) === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) { 16304 // Exit early because we don't have enough to parse 16305 // the ADTS frame header 16306 if (everything.length - byteIndex < 7) { 16307 break; 16308 } 16309 16310 frameSize = aacUtils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 16311 // to emit a full packet 16312 16313 if (byteIndex + frameSize > everything.length) { 16314 break; 16315 } 16316 16317 packet = { 16318 type: 'audio', 16319 data: everything.subarray(byteIndex, byteIndex + frameSize), 16320 pts: timeStamp, 16321 dts: timeStamp 16322 }; 16323 this.trigger('data', packet); 16324 byteIndex += frameSize; 16325 continue; 16326 } 16327 16328 byteIndex++; 16329 } 16330 16331 bytesLeft = everything.length - byteIndex; 16332 16333 if (bytesLeft > 0) { 16334 everything = everything.subarray(byteIndex); 16335 } else { 16336 everything = new Uint8Array(); 16337 } 16338 }; 16339 16340 this.reset = function () { 16341 everything = new Uint8Array(); 16342 this.trigger('reset'); 16343 }; 16344 16345 this.endTimeline = function () { 16346 everything = new Uint8Array(); 16347 this.trigger('endedtimeline'); 16348 }; 16349 }; 16350 16351 AacStream$1.prototype = new Stream$1(); 16352 var aac = AacStream$1; 16353 var AUDIO_PROPERTIES$1 = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize']; 16354 var audioProperties = AUDIO_PROPERTIES$1; 16355 var VIDEO_PROPERTIES$1 = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility', 'sarRatio']; 16356 var videoProperties = VIDEO_PROPERTIES$1; 16357 /** 16358 * mux.js 16359 * 16360 * Copyright (c) Brightcove 16361 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 16362 * 16363 * A stream-based mp2t to mp4 converter. This utility can be used to 16364 * deliver mp4s to a SourceBuffer on platforms that support native 16365 * Media Source Extensions. 16366 */ 16367 16368 var Stream = stream; 16369 var mp4 = mp4Generator; 16370 var frameUtils = frameUtils$1; 16371 var audioFrameUtils = audioFrameUtils$1; 16372 var trackDecodeInfo = trackDecodeInfo$1; 16373 var m2ts = m2ts_1; 16374 var clock = clock$2; 16375 var AdtsStream = adts; 16376 var H264Stream = h264.H264Stream; 16377 var AacStream = aac; 16378 var isLikelyAacData = utils.isLikelyAacData; 16379 var ONE_SECOND_IN_TS$1 = clock$2.ONE_SECOND_IN_TS; 16380 var AUDIO_PROPERTIES = audioProperties; 16381 var VIDEO_PROPERTIES = videoProperties; // object types 16382 16383 var VideoSegmentStream, AudioSegmentStream, Transmuxer, CoalesceStream; 16384 16385 var retriggerForStream = function (key, event) { 16386 event.stream = key; 16387 this.trigger('log', event); 16388 }; 16389 16390 var addPipelineLogRetriggers = function (transmuxer, pipeline) { 16391 var keys = Object.keys(pipeline); 16392 16393 for (var i = 0; i < keys.length; i++) { 16394 var key = keys[i]; // skip non-stream keys and headOfPipeline 16395 // which is just a duplicate 16396 16397 if (key === 'headOfPipeline' || !pipeline[key].on) { 16398 continue; 16399 } 16400 16401 pipeline[key].on('log', retriggerForStream.bind(transmuxer, key)); 16402 } 16403 }; 16404 /** 16405 * Compare two arrays (even typed) for same-ness 16406 */ 16407 16408 16409 var arrayEquals = function (a, b) { 16410 var i; 16411 16412 if (a.length !== b.length) { 16413 return false; 16414 } // compare the value of each element in the array 16415 16416 16417 for (i = 0; i < a.length; i++) { 16418 if (a[i] !== b[i]) { 16419 return false; 16420 } 16421 } 16422 16423 return true; 16424 }; 16425 16426 var generateSegmentTimingInfo = function (baseMediaDecodeTime, startDts, startPts, endDts, endPts, prependedContentDuration) { 16427 var ptsOffsetFromDts = startPts - startDts, 16428 decodeDuration = endDts - startDts, 16429 presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment, 16430 // however, the player time values will reflect a start from the baseMediaDecodeTime. 16431 // In order to provide relevant values for the player times, base timing info on the 16432 // baseMediaDecodeTime and the DTS and PTS durations of the segment. 16433 16434 return { 16435 start: { 16436 dts: baseMediaDecodeTime, 16437 pts: baseMediaDecodeTime + ptsOffsetFromDts 16438 }, 16439 end: { 16440 dts: baseMediaDecodeTime + decodeDuration, 16441 pts: baseMediaDecodeTime + presentationDuration 16442 }, 16443 prependedContentDuration: prependedContentDuration, 16444 baseMediaDecodeTime: baseMediaDecodeTime 16445 }; 16446 }; 16447 /** 16448 * Constructs a single-track, ISO BMFF media segment from AAC data 16449 * events. The output of this stream can be fed to a SourceBuffer 16450 * configured with a suitable initialization segment. 16451 * @param track {object} track metadata configuration 16452 * @param options {object} transmuxer options object 16453 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 16454 * in the source; false to adjust the first segment to start at 0. 16455 */ 16456 16457 16458 AudioSegmentStream = function (track, options) { 16459 var adtsFrames = [], 16460 sequenceNumber, 16461 earliestAllowedDts = 0, 16462 audioAppendStartTs = 0, 16463 videoBaseMediaDecodeTime = Infinity; 16464 options = options || {}; 16465 sequenceNumber = options.firstSequenceNumber || 0; 16466 AudioSegmentStream.prototype.init.call(this); 16467 16468 this.push = function (data) { 16469 trackDecodeInfo.collectDtsInfo(track, data); 16470 16471 if (track) { 16472 AUDIO_PROPERTIES.forEach(function (prop) { 16473 track[prop] = data[prop]; 16474 }); 16475 } // buffer audio data until end() is called 16476 16477 16478 adtsFrames.push(data); 16479 }; 16480 16481 this.setEarliestDts = function (earliestDts) { 16482 earliestAllowedDts = earliestDts; 16483 }; 16484 16485 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 16486 videoBaseMediaDecodeTime = baseMediaDecodeTime; 16487 }; 16488 16489 this.setAudioAppendStart = function (timestamp) { 16490 audioAppendStartTs = timestamp; 16491 }; 16492 16493 this.flush = function () { 16494 var frames, moof, mdat, boxes, frameDuration, segmentDuration, videoClockCyclesOfSilencePrefixed; // return early if no audio data has been observed 16495 16496 if (adtsFrames.length === 0) { 16497 this.trigger('done', 'AudioSegmentStream'); 16498 return; 16499 } 16500 16501 frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts); 16502 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); // amount of audio filled but the value is in video clock rather than audio clock 16503 16504 videoClockCyclesOfSilencePrefixed = audioFrameUtils.prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime); // we have to build the index from byte locations to 16505 // samples (that is, adts frames) in the audio data 16506 16507 track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat 16508 16509 mdat = mp4.mdat(audioFrameUtils.concatenateFrameData(frames)); 16510 adtsFrames = []; 16511 moof = mp4.moof(sequenceNumber, [track]); 16512 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time 16513 16514 sequenceNumber++; 16515 boxes.set(moof); 16516 boxes.set(mdat, moof.byteLength); 16517 trackDecodeInfo.clearDtsInfo(track); 16518 frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 * 1024 / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with 16519 // tests) on adding the timingInfo event. However, it seems unlikely that there's a 16520 // valid use-case where an init segment/data should be triggered without associated 16521 // frames. Leaving for now, but should be looked into. 16522 16523 if (frames.length) { 16524 segmentDuration = frames.length * frameDuration; 16525 this.trigger('segmentTimingInfo', generateSegmentTimingInfo( // The audio track's baseMediaDecodeTime is in audio clock cycles, but the 16526 // frame info is in video clock cycles. Convert to match expectation of 16527 // listeners (that all timestamps will be based on video clock cycles). 16528 clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate), // frame times are already in video clock, as is segment duration 16529 frames[0].dts, frames[0].pts, frames[0].dts + segmentDuration, frames[0].pts + segmentDuration, videoClockCyclesOfSilencePrefixed || 0)); 16530 this.trigger('timingInfo', { 16531 start: frames[0].pts, 16532 end: frames[0].pts + segmentDuration 16533 }); 16534 } 16535 16536 this.trigger('data', { 16537 track: track, 16538 boxes: boxes 16539 }); 16540 this.trigger('done', 'AudioSegmentStream'); 16541 }; 16542 16543 this.reset = function () { 16544 trackDecodeInfo.clearDtsInfo(track); 16545 adtsFrames = []; 16546 this.trigger('reset'); 16547 }; 16548 }; 16549 16550 AudioSegmentStream.prototype = new Stream(); 16551 /** 16552 * Constructs a single-track, ISO BMFF media segment from H264 data 16553 * events. The output of this stream can be fed to a SourceBuffer 16554 * configured with a suitable initialization segment. 16555 * @param track {object} track metadata configuration 16556 * @param options {object} transmuxer options object 16557 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 16558 * gopsToAlignWith list when attempting to align gop pts 16559 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 16560 * in the source; false to adjust the first segment to start at 0. 16561 */ 16562 16563 VideoSegmentStream = function (track, options) { 16564 var sequenceNumber, 16565 nalUnits = [], 16566 gopsToAlignWith = [], 16567 config, 16568 pps; 16569 options = options || {}; 16570 sequenceNumber = options.firstSequenceNumber || 0; 16571 VideoSegmentStream.prototype.init.call(this); 16572 delete track.minPTS; 16573 this.gopCache_ = []; 16574 /** 16575 * Constructs a ISO BMFF segment given H264 nalUnits 16576 * @param {Object} nalUnit A data event representing a nalUnit 16577 * @param {String} nalUnit.nalUnitType 16578 * @param {Object} nalUnit.config Properties for a mp4 track 16579 * @param {Uint8Array} nalUnit.data The nalUnit bytes 16580 * @see lib/codecs/h264.js 16581 **/ 16582 16583 this.push = function (nalUnit) { 16584 trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config 16585 16586 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 16587 config = nalUnit.config; 16588 track.sps = [nalUnit.data]; 16589 VIDEO_PROPERTIES.forEach(function (prop) { 16590 track[prop] = config[prop]; 16591 }, this); 16592 } 16593 16594 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 16595 pps = nalUnit.data; 16596 track.pps = [nalUnit.data]; 16597 } // buffer video until flush() is called 16598 16599 16600 nalUnits.push(nalUnit); 16601 }; 16602 /** 16603 * Pass constructed ISO BMFF track and boxes on to the 16604 * next stream in the pipeline 16605 **/ 16606 16607 16608 this.flush = function () { 16609 var frames, 16610 gopForFusion, 16611 gops, 16612 moof, 16613 mdat, 16614 boxes, 16615 prependedContentDuration = 0, 16616 firstGop, 16617 lastGop; // Throw away nalUnits at the start of the byte stream until 16618 // we find the first AUD 16619 16620 while (nalUnits.length) { 16621 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 16622 break; 16623 } 16624 16625 nalUnits.shift(); 16626 } // Return early if no video data has been observed 16627 16628 16629 if (nalUnits.length === 0) { 16630 this.resetStream_(); 16631 this.trigger('done', 'VideoSegmentStream'); 16632 return; 16633 } // Organize the raw nal-units into arrays that represent 16634 // higher-level constructs such as frames and gops 16635 // (group-of-pictures) 16636 16637 16638 frames = frameUtils.groupNalsIntoFrames(nalUnits); 16639 gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have 16640 // a problem since MSE (on Chrome) requires a leading keyframe. 16641 // 16642 // We have two approaches to repairing this situation: 16643 // 1) GOP-FUSION: 16644 // This is where we keep track of the GOPS (group-of-pictures) 16645 // from previous fragments and attempt to find one that we can 16646 // prepend to the current fragment in order to create a valid 16647 // fragment. 16648 // 2) KEYFRAME-PULLING: 16649 // Here we search for the first keyframe in the fragment and 16650 // throw away all the frames between the start of the fragment 16651 // and that keyframe. We then extend the duration and pull the 16652 // PTS of the keyframe forward so that it covers the time range 16653 // of the frames that were disposed of. 16654 // 16655 // #1 is far prefereable over #2 which can cause "stuttering" but 16656 // requires more things to be just right. 16657 16658 if (!gops[0][0].keyFrame) { 16659 // Search for a gop for fusion from our gopCache 16660 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 16661 16662 if (gopForFusion) { 16663 // in order to provide more accurate timing information about the segment, save 16664 // the number of seconds prepended to the original segment due to GOP fusion 16665 prependedContentDuration = gopForFusion.duration; 16666 gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the 16667 // new gop at the beginning 16668 16669 gops.byteLength += gopForFusion.byteLength; 16670 gops.nalCount += gopForFusion.nalCount; 16671 gops.pts = gopForFusion.pts; 16672 gops.dts = gopForFusion.dts; 16673 gops.duration += gopForFusion.duration; 16674 } else { 16675 // If we didn't find a candidate gop fall back to keyframe-pulling 16676 gops = frameUtils.extendFirstKeyFrame(gops); 16677 } 16678 } // Trim gops to align with gopsToAlignWith 16679 16680 16681 if (gopsToAlignWith.length) { 16682 var alignedGops; 16683 16684 if (options.alignGopsAtEnd) { 16685 alignedGops = this.alignGopsAtEnd_(gops); 16686 } else { 16687 alignedGops = this.alignGopsAtStart_(gops); 16688 } 16689 16690 if (!alignedGops) { 16691 // save all the nals in the last GOP into the gop cache 16692 this.gopCache_.unshift({ 16693 gop: gops.pop(), 16694 pps: track.pps, 16695 sps: track.sps 16696 }); // Keep a maximum of 6 GOPs in the cache 16697 16698 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 16699 16700 nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith 16701 16702 this.resetStream_(); 16703 this.trigger('done', 'VideoSegmentStream'); 16704 return; 16705 } // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 16706 // when recalculated before sending off to CoalesceStream 16707 16708 16709 trackDecodeInfo.clearDtsInfo(track); 16710 gops = alignedGops; 16711 } 16712 16713 trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to 16714 // samples (that is, frames) in the video data 16715 16716 track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat 16717 16718 mdat = mp4.mdat(frameUtils.concatenateNalData(gops)); 16719 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 16720 this.trigger('processedGopsInfo', gops.map(function (gop) { 16721 return { 16722 pts: gop.pts, 16723 dts: gop.dts, 16724 byteLength: gop.byteLength 16725 }; 16726 })); 16727 firstGop = gops[0]; 16728 lastGop = gops[gops.length - 1]; 16729 this.trigger('segmentTimingInfo', generateSegmentTimingInfo(track.baseMediaDecodeTime, firstGop.dts, firstGop.pts, lastGop.dts + lastGop.duration, lastGop.pts + lastGop.duration, prependedContentDuration)); 16730 this.trigger('timingInfo', { 16731 start: gops[0].pts, 16732 end: gops[gops.length - 1].pts + gops[gops.length - 1].duration 16733 }); // save all the nals in the last GOP into the gop cache 16734 16735 this.gopCache_.unshift({ 16736 gop: gops.pop(), 16737 pps: track.pps, 16738 sps: track.sps 16739 }); // Keep a maximum of 6 GOPs in the cache 16740 16741 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 16742 16743 nalUnits = []; 16744 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 16745 this.trigger('timelineStartInfo', track.timelineStartInfo); 16746 moof = mp4.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of 16747 // throwing away hundreds of media segment fragments 16748 16749 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time 16750 16751 sequenceNumber++; 16752 boxes.set(moof); 16753 boxes.set(mdat, moof.byteLength); 16754 this.trigger('data', { 16755 track: track, 16756 boxes: boxes 16757 }); 16758 this.resetStream_(); // Continue with the flush process now 16759 16760 this.trigger('done', 'VideoSegmentStream'); 16761 }; 16762 16763 this.reset = function () { 16764 this.resetStream_(); 16765 nalUnits = []; 16766 this.gopCache_.length = 0; 16767 gopsToAlignWith.length = 0; 16768 this.trigger('reset'); 16769 }; 16770 16771 this.resetStream_ = function () { 16772 trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments 16773 // for instance, when we are rendition switching 16774 16775 config = undefined; 16776 pps = undefined; 16777 }; // Search for a candidate Gop for gop-fusion from the gop cache and 16778 // return it or return null if no good candidate was found 16779 16780 16781 this.getGopForFusion_ = function (nalUnit) { 16782 var halfSecond = 45000, 16783 // Half-a-second in a 90khz clock 16784 allowableOverlap = 10000, 16785 // About 3 frames @ 30fps 16786 nearestDistance = Infinity, 16787 dtsDistance, 16788 nearestGopObj, 16789 currentGop, 16790 currentGopObj, 16791 i; // Search for the GOP nearest to the beginning of this nal unit 16792 16793 for (i = 0; i < this.gopCache_.length; i++) { 16794 currentGopObj = this.gopCache_[i]; 16795 currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS 16796 16797 if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) { 16798 continue; 16799 } // Reject Gops that would require a negative baseMediaDecodeTime 16800 16801 16802 if (currentGop.dts < track.timelineStartInfo.dts) { 16803 continue; 16804 } // The distance between the end of the gop and the start of the nalUnit 16805 16806 16807 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within 16808 // a half-second of the nal unit 16809 16810 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 16811 // Always use the closest GOP we found if there is more than 16812 // one candidate 16813 if (!nearestGopObj || nearestDistance > dtsDistance) { 16814 nearestGopObj = currentGopObj; 16815 nearestDistance = dtsDistance; 16816 } 16817 } 16818 } 16819 16820 if (nearestGopObj) { 16821 return nearestGopObj.gop; 16822 } 16823 16824 return null; 16825 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 16826 // of gopsToAlignWith starting from the START of the list 16827 16828 16829 this.alignGopsAtStart_ = function (gops) { 16830 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 16831 byteLength = gops.byteLength; 16832 nalCount = gops.nalCount; 16833 duration = gops.duration; 16834 alignIndex = gopIndex = 0; 16835 16836 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 16837 align = gopsToAlignWith[alignIndex]; 16838 gop = gops[gopIndex]; 16839 16840 if (align.pts === gop.pts) { 16841 break; 16842 } 16843 16844 if (gop.pts > align.pts) { 16845 // this current gop starts after the current gop we want to align on, so increment 16846 // align index 16847 alignIndex++; 16848 continue; 16849 } // current gop starts before the current gop we want to align on. so increment gop 16850 // index 16851 16852 16853 gopIndex++; 16854 byteLength -= gop.byteLength; 16855 nalCount -= gop.nalCount; 16856 duration -= gop.duration; 16857 } 16858 16859 if (gopIndex === 0) { 16860 // no gops to trim 16861 return gops; 16862 } 16863 16864 if (gopIndex === gops.length) { 16865 // all gops trimmed, skip appending all gops 16866 return null; 16867 } 16868 16869 alignedGops = gops.slice(gopIndex); 16870 alignedGops.byteLength = byteLength; 16871 alignedGops.duration = duration; 16872 alignedGops.nalCount = nalCount; 16873 alignedGops.pts = alignedGops[0].pts; 16874 alignedGops.dts = alignedGops[0].dts; 16875 return alignedGops; 16876 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 16877 // of gopsToAlignWith starting from the END of the list 16878 16879 16880 this.alignGopsAtEnd_ = function (gops) { 16881 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 16882 alignIndex = gopsToAlignWith.length - 1; 16883 gopIndex = gops.length - 1; 16884 alignEndIndex = null; 16885 matchFound = false; 16886 16887 while (alignIndex >= 0 && gopIndex >= 0) { 16888 align = gopsToAlignWith[alignIndex]; 16889 gop = gops[gopIndex]; 16890 16891 if (align.pts === gop.pts) { 16892 matchFound = true; 16893 break; 16894 } 16895 16896 if (align.pts > gop.pts) { 16897 alignIndex--; 16898 continue; 16899 } 16900 16901 if (alignIndex === gopsToAlignWith.length - 1) { 16902 // gop.pts is greater than the last alignment candidate. If no match is found 16903 // by the end of this loop, we still want to append gops that come after this 16904 // point 16905 alignEndIndex = gopIndex; 16906 } 16907 16908 gopIndex--; 16909 } 16910 16911 if (!matchFound && alignEndIndex === null) { 16912 return null; 16913 } 16914 16915 var trimIndex; 16916 16917 if (matchFound) { 16918 trimIndex = gopIndex; 16919 } else { 16920 trimIndex = alignEndIndex; 16921 } 16922 16923 if (trimIndex === 0) { 16924 return gops; 16925 } 16926 16927 var alignedGops = gops.slice(trimIndex); 16928 var metadata = alignedGops.reduce(function (total, gop) { 16929 total.byteLength += gop.byteLength; 16930 total.duration += gop.duration; 16931 total.nalCount += gop.nalCount; 16932 return total; 16933 }, { 16934 byteLength: 0, 16935 duration: 0, 16936 nalCount: 0 16937 }); 16938 alignedGops.byteLength = metadata.byteLength; 16939 alignedGops.duration = metadata.duration; 16940 alignedGops.nalCount = metadata.nalCount; 16941 alignedGops.pts = alignedGops[0].pts; 16942 alignedGops.dts = alignedGops[0].dts; 16943 return alignedGops; 16944 }; 16945 16946 this.alignGopsWith = function (newGopsToAlignWith) { 16947 gopsToAlignWith = newGopsToAlignWith; 16948 }; 16949 }; 16950 16951 VideoSegmentStream.prototype = new Stream(); 16952 /** 16953 * A Stream that can combine multiple streams (ie. audio & video) 16954 * into a single output segment for MSE. Also supports audio-only 16955 * and video-only streams. 16956 * @param options {object} transmuxer options object 16957 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 16958 * in the source; false to adjust the first segment to start at media timeline start. 16959 */ 16960 16961 CoalesceStream = function (options, metadataStream) { 16962 // Number of Tracks per output segment 16963 // If greater than 1, we combine multiple 16964 // tracks into a single segment 16965 this.numberOfTracks = 0; 16966 this.metadataStream = metadataStream; 16967 options = options || {}; 16968 16969 if (typeof options.remux !== 'undefined') { 16970 this.remuxTracks = !!options.remux; 16971 } else { 16972 this.remuxTracks = true; 16973 } 16974 16975 if (typeof options.keepOriginalTimestamps === 'boolean') { 16976 this.keepOriginalTimestamps = options.keepOriginalTimestamps; 16977 } else { 16978 this.keepOriginalTimestamps = false; 16979 } 16980 16981 this.pendingTracks = []; 16982 this.videoTrack = null; 16983 this.pendingBoxes = []; 16984 this.pendingCaptions = []; 16985 this.pendingMetadata = []; 16986 this.pendingBytes = 0; 16987 this.emittedTracks = 0; 16988 CoalesceStream.prototype.init.call(this); // Take output from multiple 16989 16990 this.push = function (output) { 16991 // buffer incoming captions until the associated video segment 16992 // finishes 16993 if (output.content || output.text) { 16994 return this.pendingCaptions.push(output); 16995 } // buffer incoming id3 tags until the final flush 16996 16997 16998 if (output.frames) { 16999 return this.pendingMetadata.push(output); 17000 } // Add this track to the list of pending tracks and store 17001 // important information required for the construction of 17002 // the final segment 17003 17004 17005 this.pendingTracks.push(output.track); 17006 this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome? 17007 // We unshift audio and push video because 17008 // as of Chrome 75 when switching from 17009 // one init segment to another if the video 17010 // mdat does not appear after the audio mdat 17011 // only audio will play for the duration of our transmux. 17012 17013 if (output.track.type === 'video') { 17014 this.videoTrack = output.track; 17015 this.pendingBoxes.push(output.boxes); 17016 } 17017 17018 if (output.track.type === 'audio') { 17019 this.audioTrack = output.track; 17020 this.pendingBoxes.unshift(output.boxes); 17021 } 17022 }; 17023 }; 17024 17025 CoalesceStream.prototype = new Stream(); 17026 17027 CoalesceStream.prototype.flush = function (flushSource) { 17028 var offset = 0, 17029 event = { 17030 captions: [], 17031 captionStreams: {}, 17032 metadata: [], 17033 info: {} 17034 }, 17035 caption, 17036 id3, 17037 initSegment, 17038 timelineStartPts = 0, 17039 i; 17040 17041 if (this.pendingTracks.length < this.numberOfTracks) { 17042 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 17043 // Return because we haven't received a flush from a data-generating 17044 // portion of the segment (meaning that we have only recieved meta-data 17045 // or captions.) 17046 return; 17047 } else if (this.remuxTracks) { 17048 // Return until we have enough tracks from the pipeline to remux (if we 17049 // are remuxing audio and video into a single MP4) 17050 return; 17051 } else if (this.pendingTracks.length === 0) { 17052 // In the case where we receive a flush without any data having been 17053 // received we consider it an emitted track for the purposes of coalescing 17054 // `done` events. 17055 // We do this for the case where there is an audio and video track in the 17056 // segment but no audio data. (seen in several playlists with alternate 17057 // audio tracks and no audio present in the main TS segments.) 17058 this.emittedTracks++; 17059 17060 if (this.emittedTracks >= this.numberOfTracks) { 17061 this.trigger('done'); 17062 this.emittedTracks = 0; 17063 } 17064 17065 return; 17066 } 17067 } 17068 17069 if (this.videoTrack) { 17070 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 17071 VIDEO_PROPERTIES.forEach(function (prop) { 17072 event.info[prop] = this.videoTrack[prop]; 17073 }, this); 17074 } else if (this.audioTrack) { 17075 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 17076 AUDIO_PROPERTIES.forEach(function (prop) { 17077 event.info[prop] = this.audioTrack[prop]; 17078 }, this); 17079 } 17080 17081 if (this.videoTrack || this.audioTrack) { 17082 if (this.pendingTracks.length === 1) { 17083 event.type = this.pendingTracks[0].type; 17084 } else { 17085 event.type = 'combined'; 17086 } 17087 17088 this.emittedTracks += this.pendingTracks.length; 17089 initSegment = mp4.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment 17090 17091 event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov 17092 // and track definitions 17093 17094 event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats 17095 17096 event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together 17097 17098 for (i = 0; i < this.pendingBoxes.length; i++) { 17099 event.data.set(this.pendingBoxes[i], offset); 17100 offset += this.pendingBoxes[i].byteLength; 17101 } // Translate caption PTS times into second offsets to match the 17102 // video timeline for the segment, and add track info 17103 17104 17105 for (i = 0; i < this.pendingCaptions.length; i++) { 17106 caption = this.pendingCaptions[i]; 17107 caption.startTime = clock.metadataTsToSeconds(caption.startPts, timelineStartPts, this.keepOriginalTimestamps); 17108 caption.endTime = clock.metadataTsToSeconds(caption.endPts, timelineStartPts, this.keepOriginalTimestamps); 17109 event.captionStreams[caption.stream] = true; 17110 event.captions.push(caption); 17111 } // Translate ID3 frame PTS times into second offsets to match the 17112 // video timeline for the segment 17113 17114 17115 for (i = 0; i < this.pendingMetadata.length; i++) { 17116 id3 = this.pendingMetadata[i]; 17117 id3.cueTime = clock.metadataTsToSeconds(id3.pts, timelineStartPts, this.keepOriginalTimestamps); 17118 event.metadata.push(id3); 17119 } // We add this to every single emitted segment even though we only need 17120 // it for the first 17121 17122 17123 event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state 17124 17125 this.pendingTracks.length = 0; 17126 this.videoTrack = null; 17127 this.pendingBoxes.length = 0; 17128 this.pendingCaptions.length = 0; 17129 this.pendingBytes = 0; 17130 this.pendingMetadata.length = 0; // Emit the built segment 17131 // We include captions and ID3 tags for backwards compatibility, 17132 // ideally we should send only video and audio in the data event 17133 17134 this.trigger('data', event); // Emit each caption to the outside world 17135 // Ideally, this would happen immediately on parsing captions, 17136 // but we need to ensure that video data is sent back first 17137 // so that caption timing can be adjusted to match video timing 17138 17139 for (i = 0; i < event.captions.length; i++) { 17140 caption = event.captions[i]; 17141 this.trigger('caption', caption); 17142 } // Emit each id3 tag to the outside world 17143 // Ideally, this would happen immediately on parsing the tag, 17144 // but we need to ensure that video data is sent back first 17145 // so that ID3 frame timing can be adjusted to match video timing 17146 17147 17148 for (i = 0; i < event.metadata.length; i++) { 17149 id3 = event.metadata[i]; 17150 this.trigger('id3Frame', id3); 17151 } 17152 } // Only emit `done` if all tracks have been flushed and emitted 17153 17154 17155 if (this.emittedTracks >= this.numberOfTracks) { 17156 this.trigger('done'); 17157 this.emittedTracks = 0; 17158 } 17159 }; 17160 17161 CoalesceStream.prototype.setRemux = function (val) { 17162 this.remuxTracks = val; 17163 }; 17164 /** 17165 * A Stream that expects MP2T binary data as input and produces 17166 * corresponding media segments, suitable for use with Media Source 17167 * Extension (MSE) implementations that support the ISO BMFF byte 17168 * stream format, like Chrome. 17169 */ 17170 17171 17172 Transmuxer = function (options) { 17173 var self = this, 17174 hasFlushed = true, 17175 videoTrack, 17176 audioTrack; 17177 Transmuxer.prototype.init.call(this); 17178 options = options || {}; 17179 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 17180 this.transmuxPipeline_ = {}; 17181 17182 this.setupAacPipeline = function () { 17183 var pipeline = {}; 17184 this.transmuxPipeline_ = pipeline; 17185 pipeline.type = 'aac'; 17186 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 17187 17188 pipeline.aacStream = new AacStream(); 17189 pipeline.audioTimestampRolloverStream = new m2ts.TimestampRolloverStream('audio'); 17190 pipeline.timedMetadataTimestampRolloverStream = new m2ts.TimestampRolloverStream('timed-metadata'); 17191 pipeline.adtsStream = new AdtsStream(); 17192 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 17193 pipeline.headOfPipeline = pipeline.aacStream; 17194 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 17195 pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 17196 pipeline.metadataStream.on('timestamp', function (frame) { 17197 pipeline.aacStream.setTimestamp(frame.timeStamp); 17198 }); 17199 pipeline.aacStream.on('data', function (data) { 17200 if (data.type !== 'timed-metadata' && data.type !== 'audio' || pipeline.audioSegmentStream) { 17201 return; 17202 } 17203 17204 audioTrack = audioTrack || { 17205 timelineStartInfo: { 17206 baseMediaDecodeTime: self.baseMediaDecodeTime 17207 }, 17208 codec: 'adts', 17209 type: 'audio' 17210 }; // hook up the audio segment stream to the first track with aac data 17211 17212 pipeline.coalesceStream.numberOfTracks++; 17213 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 17214 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 17215 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline 17216 17217 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); // emit pmt info 17218 17219 self.trigger('trackinfo', { 17220 hasAudio: !!audioTrack, 17221 hasVideo: !!videoTrack 17222 }); 17223 }); // Re-emit any data coming from the coalesce stream to the outside world 17224 17225 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline 17226 17227 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 17228 addPipelineLogRetriggers(this, pipeline); 17229 }; 17230 17231 this.setupTsPipeline = function () { 17232 var pipeline = {}; 17233 this.transmuxPipeline_ = pipeline; 17234 pipeline.type = 'ts'; 17235 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 17236 17237 pipeline.packetStream = new m2ts.TransportPacketStream(); 17238 pipeline.parseStream = new m2ts.TransportParseStream(); 17239 pipeline.elementaryStream = new m2ts.ElementaryStream(); 17240 pipeline.timestampRolloverStream = new m2ts.TimestampRolloverStream(); 17241 pipeline.adtsStream = new AdtsStream(); 17242 pipeline.h264Stream = new H264Stream(); 17243 pipeline.captionStream = new m2ts.CaptionStream(options); 17244 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 17245 pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams 17246 17247 pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream).pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!! 17248 // demux the streams 17249 17250 pipeline.timestampRolloverStream.pipe(pipeline.h264Stream); 17251 pipeline.timestampRolloverStream.pipe(pipeline.adtsStream); 17252 pipeline.timestampRolloverStream.pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream 17253 17254 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 17255 pipeline.elementaryStream.on('data', function (data) { 17256 var i; 17257 17258 if (data.type === 'metadata') { 17259 i = data.tracks.length; // scan the tracks listed in the metadata 17260 17261 while (i--) { 17262 if (!videoTrack && data.tracks[i].type === 'video') { 17263 videoTrack = data.tracks[i]; 17264 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 17265 } else if (!audioTrack && data.tracks[i].type === 'audio') { 17266 audioTrack = data.tracks[i]; 17267 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 17268 } 17269 } // hook up the video segment stream to the first track with h264 data 17270 17271 17272 if (videoTrack && !pipeline.videoSegmentStream) { 17273 pipeline.coalesceStream.numberOfTracks++; 17274 pipeline.videoSegmentStream = new VideoSegmentStream(videoTrack, options); 17275 pipeline.videoSegmentStream.on('log', self.getLogTrigger_('videoSegmentStream')); 17276 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 17277 // When video emits timelineStartInfo data after a flush, we forward that 17278 // info to the AudioSegmentStream, if it exists, because video timeline 17279 // data takes precedence. Do not do this if keepOriginalTimestamps is set, 17280 // because this is a particularly subtle form of timestamp alteration. 17281 if (audioTrack && !options.keepOriginalTimestamps) { 17282 audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the 17283 // very earliest DTS we have seen in video because Chrome will 17284 // interpret any video track with a baseMediaDecodeTime that is 17285 // non-zero as a gap. 17286 17287 pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts - self.baseMediaDecodeTime); 17288 } 17289 }); 17290 pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo')); 17291 pipeline.videoSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'videoSegmentTimingInfo')); 17292 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 17293 if (audioTrack) { 17294 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 17295 } 17296 }); 17297 pipeline.videoSegmentStream.on('timingInfo', self.trigger.bind(self, 'videoTimingInfo')); // Set up the final part of the video pipeline 17298 17299 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 17300 } 17301 17302 if (audioTrack && !pipeline.audioSegmentStream) { 17303 // hook up the audio segment stream to the first track with aac data 17304 pipeline.coalesceStream.numberOfTracks++; 17305 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 17306 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 17307 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); 17308 pipeline.audioSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'audioSegmentTimingInfo')); // Set up the final part of the audio pipeline 17309 17310 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 17311 } // emit pmt info 17312 17313 17314 self.trigger('trackinfo', { 17315 hasAudio: !!audioTrack, 17316 hasVideo: !!videoTrack 17317 }); 17318 } 17319 }); // Re-emit any data coming from the coalesce stream to the outside world 17320 17321 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 17322 pipeline.coalesceStream.on('id3Frame', function (id3Frame) { 17323 id3Frame.dispatchType = pipeline.metadataStream.dispatchType; 17324 self.trigger('id3Frame', id3Frame); 17325 }); 17326 pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline 17327 17328 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 17329 addPipelineLogRetriggers(this, pipeline); 17330 }; // hook up the segment streams once track metadata is delivered 17331 17332 17333 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 17334 var pipeline = this.transmuxPipeline_; 17335 17336 if (!options.keepOriginalTimestamps) { 17337 this.baseMediaDecodeTime = baseMediaDecodeTime; 17338 } 17339 17340 if (audioTrack) { 17341 audioTrack.timelineStartInfo.dts = undefined; 17342 audioTrack.timelineStartInfo.pts = undefined; 17343 trackDecodeInfo.clearDtsInfo(audioTrack); 17344 17345 if (pipeline.audioTimestampRolloverStream) { 17346 pipeline.audioTimestampRolloverStream.discontinuity(); 17347 } 17348 } 17349 17350 if (videoTrack) { 17351 if (pipeline.videoSegmentStream) { 17352 pipeline.videoSegmentStream.gopCache_ = []; 17353 } 17354 17355 videoTrack.timelineStartInfo.dts = undefined; 17356 videoTrack.timelineStartInfo.pts = undefined; 17357 trackDecodeInfo.clearDtsInfo(videoTrack); 17358 pipeline.captionStream.reset(); 17359 } 17360 17361 if (pipeline.timestampRolloverStream) { 17362 pipeline.timestampRolloverStream.discontinuity(); 17363 } 17364 }; 17365 17366 this.setAudioAppendStart = function (timestamp) { 17367 if (audioTrack) { 17368 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 17369 } 17370 }; 17371 17372 this.setRemux = function (val) { 17373 var pipeline = this.transmuxPipeline_; 17374 options.remux = val; 17375 17376 if (pipeline && pipeline.coalesceStream) { 17377 pipeline.coalesceStream.setRemux(val); 17378 } 17379 }; 17380 17381 this.alignGopsWith = function (gopsToAlignWith) { 17382 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 17383 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 17384 } 17385 }; 17386 17387 this.getLogTrigger_ = function (key) { 17388 var self = this; 17389 return function (event) { 17390 event.stream = key; 17391 self.trigger('log', event); 17392 }; 17393 }; // feed incoming data to the front of the parsing pipeline 17394 17395 17396 this.push = function (data) { 17397 if (hasFlushed) { 17398 var isAac = isLikelyAacData(data); 17399 17400 if (isAac && this.transmuxPipeline_.type !== 'aac') { 17401 this.setupAacPipeline(); 17402 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 17403 this.setupTsPipeline(); 17404 } 17405 17406 hasFlushed = false; 17407 } 17408 17409 this.transmuxPipeline_.headOfPipeline.push(data); 17410 }; // flush any buffered data 17411 17412 17413 this.flush = function () { 17414 hasFlushed = true; // Start at the top of the pipeline and flush all pending work 17415 17416 this.transmuxPipeline_.headOfPipeline.flush(); 17417 }; 17418 17419 this.endTimeline = function () { 17420 this.transmuxPipeline_.headOfPipeline.endTimeline(); 17421 }; 17422 17423 this.reset = function () { 17424 if (this.transmuxPipeline_.headOfPipeline) { 17425 this.transmuxPipeline_.headOfPipeline.reset(); 17426 } 17427 }; // Caption data has to be reset when seeking outside buffered range 17428 17429 17430 this.resetCaptions = function () { 17431 if (this.transmuxPipeline_.captionStream) { 17432 this.transmuxPipeline_.captionStream.reset(); 17433 } 17434 }; 17435 }; 17436 17437 Transmuxer.prototype = new Stream(); 17438 var transmuxer = { 17439 Transmuxer: Transmuxer, 17440 VideoSegmentStream: VideoSegmentStream, 17441 AudioSegmentStream: AudioSegmentStream, 17442 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 17443 VIDEO_PROPERTIES: VIDEO_PROPERTIES, 17444 // exported for testing 17445 generateSegmentTimingInfo: generateSegmentTimingInfo 17446 }; 17447 /** 17448 * mux.js 17449 * 17450 * Copyright (c) Brightcove 17451 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 17452 */ 17453 17454 var toUnsigned$3 = function (value) { 17455 return value >>> 0; 17456 }; 17457 17458 var toHexString$1 = function (value) { 17459 return ('00' + value.toString(16)).slice(-2); 17460 }; 17461 17462 var bin = { 17463 toUnsigned: toUnsigned$3, 17464 toHexString: toHexString$1 17465 }; 17466 17467 var parseType$
vendor: 4,085 bytes, lines 17467-17599
174673 = function (buffer) { 17468 var result = ''; 17469 result += String.fromCharCode(buffer[0]); 17470 result += String.fromCharCode(buffer[1]); 17471 result += String.fromCharCode(buffer[2]); 17472 result += String.fromCharCode(buffer[3]); 17473 return result; 17474 }; 17475 17476 var parseType_1 = parseType$3; 17477 var toUnsigned$2 = bin.toUnsigned; 17478 var parseType$2 = parseType_1; 17479 17480 var findBox$2 = function (data, path) { 17481 var results = [], 17482 i, 17483 size, 17484 type, 17485 end, 17486 subresults; 17487 17488 if (!path.length) { 17489 // short-circuit the search for empty paths 17490 return null; 17491 } 17492 17493 for (i = 0; i < data.byteLength;) { 17494 size = toUnsigned$2(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]); 17495 type = parseType$2(data.subarray(i + 4, i + 8)); 17496 end = size > 1 ? i + size : data.byteLength; 17497 17498 if (type === path[0]) { 17499 if (path.length === 1) { 17500 // this is the end of the path and we've found the box we were 17501 // looking for 17502 results.push(data.subarray(i + 8, end)); 17503 } else { 17504 // recursively search for the next box along the path 17505 subresults = findBox$2(data.subarray(i + 8, end), path.slice(1)); 17506 17507 if (subresults.length) { 17508 results = results.concat(subresults); 17509 } 17510 } 17511 } 17512 17513 i = end; 17514 } // we've finished searching all of data 17515 17516 17517 return results; 17518 }; 17519 17520 var findBox_1 = findBox$2; 17521 var toUnsigned$1 = bin.toUnsigned; 17522 var getUint64$2 = numbers.getUint64; 17523 17524 var tfdt = function (data) { 17525 var result = { 17526 version: data[0], 17527 flags: new Uint8Array(data.subarray(1, 4)) 17528 }; 17529 17530 if (result.version === 1) { 17531 result.baseMediaDecodeTime = getUint64$2(data.subarray(4)); 17532 } else { 17533 result.baseMediaDecodeTime = toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]); 17534 } 17535 17536 return result; 17537 }; 17538 17539 var parseTfdt$2 = tfdt; 17540 17541 var parseSampleFlags$1 = function (flags) { 17542 return { 17543 isLeading: (flags[0] & 0x0c) >>> 2, 17544 dependsOn: flags[0] & 0x03, 17545 isDependedOn: (flags[1] & 0xc0) >>> 6, 17546 hasRedundancy: (flags[1] & 0x30) >>> 4, 17547 paddingValue: (flags[1] & 0x0e) >>> 1, 17548 isNonSyncSample: flags[1] & 0x01, 17549 degradationPriority: flags[2] << 8 | flags[3] 17550 }; 17551 }; 17552 17553 var parseSampleFlags_1 = parseSampleFlags$1; 17554 var parseSampleFlags = parseSampleFlags_1; 17555 17556 var trun = function (data) { 17557 var result = { 17558 version: data[0], 17559 flags: new Uint8Array(data.subarray(1, 4)), 17560 samples: [] 17561 }, 17562 view = new DataView(data.buffer, data.byteOffset, data.byteLength), 17563 // Flag interpretation 17564 dataOffsetPresent = result.flags[2] & 0x01, 17565 // compare with 2nd byte of 0x1 17566 firstSampleFlagsPresent = result.flags[2] & 0x04, 17567 // compare with 2nd byte of 0x4 17568 sampleDurationPresent = result.flags[1] & 0x01, 17569 // compare with 2nd byte of 0x100 17570 sampleSizePresent = result.flags[1] & 0x02, 17571 // compare with 2nd byte of 0x200 17572 sampleFlagsPresent = result.flags[1] & 0x04, 17573 // compare with 2nd byte of 0x400 17574 sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, 17575 // compare with 2nd byte of 0x800 17576 sampleCount = view.getUint32(4), 17577 offset = 8, 17578 sample; 17579 17580 if (dataOffsetPresent) { 17581 // 32 bit signed integer 17582 result.dataOffset = view.getInt32(offset); 17583 offset += 4; 17584 } // Overrides the flags for the first sample only. The order of 17585 // optional values will be: duration, size, compositionTimeOffset 17586 17587 17588 if (firstSampleFlagsPresent && sampleCount) { 17589 sample = { 17590 flags: parseSampleFlags(data.subarray(offset, offset + 4)) 17591 }; 17592 offset += 4; 17593 17594 if (sampleDurationPresent) { 17595 sample.duration = view.getUint32(offset); 17596 offset += 4; 17597 } 17598 17599 if (sampleSizePresent) {
vendor: 4,898 bytes, lines 17600-17773
17600 sample.size = view.getUint32(offset); 17601 offset += 4; 17602 } 17603 17604 if (sampleCompositionTimeOffsetPresent) { 17605 if (result.version === 1) { 17606 sample.compositionTimeOffset = view.getInt32(offset); 17607 } else { 17608 sample.compositionTimeOffset = view.getUint32(offset); 17609 } 17610 17611 offset += 4; 17612 } 17613 17614 result.samples.push(sample); 17615 sampleCount--; 17616 } 17617 17618 while (sampleCount--) { 17619 sample = {}; 17620 17621 if (sampleDurationPresent) { 17622 sample.duration = view.getUint32(offset); 17623 offset += 4; 17624 } 17625 17626 if (sampleSizePresent) { 17627 sample.size = view.getUint32(offset); 17628 offset += 4; 17629 } 17630 17631 if (sampleFlagsPresent) { 17632 sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); 17633 offset += 4; 17634 } 17635 17636 if (sampleCompositionTimeOffsetPresent) { 17637 if (result.version === 1) { 17638 sample.compositionTimeOffset = view.getInt32(offset); 17639 } else { 17640 sample.compositionTimeOffset = view.getUint32(offset); 17641 } 17642 17643 offset += 4; 17644 } 17645 17646 result.samples.push(sample); 17647 } 17648 17649 return result; 17650 }; 17651 17652 var parseTrun$2 = trun; 17653 17654 var tfhd = function (data) { 17655 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 17656 result = { 17657 version: data[0], 17658 flags: new Uint8Array(data.subarray(1, 4)), 17659 trackId: view.getUint32(4) 17660 }, 17661 baseDataOffsetPresent = result.flags[2] & 0x01, 17662 sampleDescriptionIndexPresent = result.flags[2] & 0x02, 17663 defaultSampleDurationPresent = result.flags[2] & 0x08, 17664 defaultSampleSizePresent = result.flags[2] & 0x10, 17665 defaultSampleFlagsPresent = result.flags[2] & 0x20, 17666 durationIsEmpty = result.flags[0] & 0x010000, 17667 defaultBaseIsMoof = result.flags[0] & 0x020000, 17668 i; 17669 i = 8; 17670 17671 if (baseDataOffsetPresent) { 17672 i += 4; // truncate top 4 bytes 17673 // FIXME: should we read the full 64 bits? 17674 17675 result.baseDataOffset = view.getUint32(12); 17676 i += 4; 17677 } 17678 17679 if (sampleDescriptionIndexPresent) { 17680 result.sampleDescriptionIndex = view.getUint32(i); 17681 i += 4; 17682 } 17683 17684 if (defaultSampleDurationPresent) { 17685 result.defaultSampleDuration = view.getUint32(i); 17686 i += 4; 17687 } 17688 17689 if (defaultSampleSizePresent) { 17690 result.defaultSampleSize = view.getUint32(i); 17691 i += 4; 17692 } 17693 17694 if (defaultSampleFlagsPresent) { 17695 result.defaultSampleFlags = view.getUint32(i); 17696 } 17697 17698 if (durationIsEmpty) { 17699 result.durationIsEmpty = true; 17700 } 17701 17702 if (!baseDataOffsetPresent && defaultBaseIsMoof) { 17703 result.baseDataOffsetIsMoof = true; 17704 } 17705 17706 return result; 17707 }; 17708 17709 var parseTfhd$2 = tfhd; 17710 var win; 17711 17712 if (typeof window !== "undefined") { 17713 win = window; 17714 } else if (typeof commonjsGlobal !== "undefined") { 17715 win = commonjsGlobal; 17716 } else if (typeof self !== "undefined") { 17717 win = self; 17718 } else { 17719 win = {}; 17720 } 17721 17722 var window_1 = win; 17723 /** 17724 * mux.js 17725 * 17726 * Copyright (c) Brightcove 17727 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 17728 * 17729 * Reads in-band CEA-708 captions out of FMP4 segments. 17730 * @see https://en.wikipedia.org/wiki/CEA-708 17731 */ 17732 17733 var discardEmulationPreventionBytes = captionPacketParser.discardEmulationPreventionBytes; 17734 var CaptionStream = captionStream.CaptionStream; 17735 var findBox$1 = findBox_1; 17736 var parseTfdt$1 = parseTfdt$2; 17737 var parseTrun$1 = parseTrun$2; 17738 var parseTfhd$1 = parseTfhd$2; 17739 var window$2 = window_1; 17740 /** 17741 * Maps an offset in the mdat to a sample based on the the size of the samples. 17742 * Assumes that `parseSamples` has been called first. 17743 * 17744 * @param {Number} offset - The offset into the mdat 17745 * @param {Object[]} samples - An array of samples, parsed using `parseSamples` 17746 * @return {?Object} The matching sample, or null if no match was found. 17747 * 17748 * @see ISO-BMFF-12/2015, Section 8.8.8 17749 **/ 17750 17751 var mapToSample = function (offset, samples) { 17752 var approximateOffset = offset; 17753 17754 for (var i = 0; i < samples.length; i++) { 17755 var sample = samples[i]; 17756 17757 if (approximateOffset < sample.size) { 17758 return sample; 17759 } 17760 17761 approximateOffset -= sample.size; 17762 } 17763 17764 return null; 17765 }; 17766 /** 17767 * Finds SEI nal units contained in a Media Data Box. 17768 * Assumes that `parseSamples` has been called first. 17769 * 17770 * @param {Uint8Array} avcStream - The bytes of the mdat 17771 * @param {Object[]} samples - The samples parsed out by `parseSamples` 17772 * @param {Number} trackId - The trackId of this video track 17773 * @return {Object[]}
vendor: 19,704 bytes, lines 17773-18351
17773 seiNals - the parsed SEI NALUs found. 17774 * The contents of the seiNal should match what is expected by 17775 * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts) 17776 * 17777 * @see ISO-BMFF-12/2015, Section 8.1.1 17778 * @see Rec. ITU-T H.264, 7.3.2.3.1 17779 **/ 17780 17781 17782 var findSeiNals = function (avcStream, samples, trackId) { 17783 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 17784 result = { 17785 logs: [], 17786 seiNals: [] 17787 }, 17788 seiNal, 17789 i, 17790 length, 17791 lastMatchedSample; 17792 17793 for (i = 0; i + 4 < avcStream.length; i += length) { 17794 length = avcView.getUint32(i); 17795 i += 4; // Bail if this doesn't appear to be an H264 stream 17796 17797 if (length <= 0) { 17798 continue; 17799 } 17800 17801 switch (avcStream[i] & 0x1F) { 17802 case 0x06: 17803 var data = avcStream.subarray(i + 1, i + 1 + length); 17804 var matchingSample = mapToSample(i, samples); 17805 seiNal = { 17806 nalUnitType: 'sei_rbsp', 17807 size: length, 17808 data: data, 17809 escapedRBSP: discardEmulationPreventionBytes(data), 17810 trackId: trackId 17811 }; 17812 17813 if (matchingSample) { 17814 seiNal.pts = matchingSample.pts; 17815 seiNal.dts = matchingSample.dts; 17816 lastMatchedSample = matchingSample; 17817 } else if (lastMatchedSample) { 17818 // If a matching sample cannot be found, use the last 17819 // sample's values as they should be as close as possible 17820 seiNal.pts = lastMatchedSample.pts; 17821 seiNal.dts = lastMatchedSample.dts; 17822 } else { 17823 result.logs.push({ 17824 level: 'warn', 17825 message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.' 17826 }); 17827 break; 17828 } 17829 17830 result.seiNals.push(seiNal); 17831 break; 17832 } 17833 } 17834 17835 return result; 17836 }; 17837 /** 17838 * Parses sample information out of Track Run Boxes and calculates 17839 * the absolute presentation and decode timestamps of each sample. 17840 * 17841 * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed 17842 * @param {Number|BigInt} baseMediaDecodeTime - base media decode time from tfdt 17843 @see ISO-BMFF-12/2015, Section 8.8.12 17844 * @param {Object} tfhd - The parsed Track Fragment Header 17845 * @see inspect.parseTfhd 17846 * @return {Object[]} the parsed samples 17847 * 17848 * @see ISO-BMFF-12/2015, Section 8.8.8 17849 **/ 17850 17851 17852 var parseSamples = function (truns, baseMediaDecodeTime, tfhd) { 17853 var currentDts = baseMediaDecodeTime; 17854 var defaultSampleDuration = tfhd.defaultSampleDuration || 0; 17855 var defaultSampleSize = tfhd.defaultSampleSize || 0; 17856 var trackId = tfhd.trackId; 17857 var allSamples = []; 17858 truns.forEach(function (trun) { 17859 // Note: We currently do not parse the sample table as well 17860 // as the trun. It's possible some sources will require this. 17861 // moov > trak > mdia > minf > stbl 17862 var trackRun = parseTrun$1(trun); 17863 var samples = trackRun.samples; 17864 samples.forEach(function (sample) { 17865 if (sample.duration === undefined) { 17866 sample.duration = defaultSampleDuration; 17867 } 17868 17869 if (sample.size === undefined) { 17870 sample.size = defaultSampleSize; 17871 } 17872 17873 sample.trackId = trackId; 17874 sample.dts = currentDts; 17875 17876 if (sample.compositionTimeOffset === undefined) { 17877 sample.compositionTimeOffset = 0; 17878 } 17879 17880 if (typeof currentDts === 'bigint') { 17881 sample.pts = currentDts + window$2.BigInt(sample.compositionTimeOffset); 17882 currentDts += window$2.BigInt(sample.duration); 17883 } else { 17884 sample.pts = currentDts + sample.compositionTimeOffset; 17885 currentDts += sample.duration; 17886 } 17887 }); 17888 allSamples = allSamples.concat(samples); 17889 }); 17890 return allSamples; 17891 }; 17892 /** 17893 * Parses out caption nals from an FMP4 segment's video tracks. 17894 * 17895 * @param {Uint8Array} segment - The bytes of a single segment 17896 * @param {Number} videoTrackId - The trackId of a video track in the segment 17897 * @return {Object.<Number, Object[]>} A mapping of video trackId to 17898 * a list of seiNals found in that track 17899 **/ 17900 17901 17902 var parseCaptionNals = function (segment, videoTrackId) { 17903 // To get the samples 17904 var trafs = findBox$1(segment, ['moof', 'traf']); // To get SEI NAL units 17905 17906 var mdats = findBox$1(segment, ['mdat']); 17907 var captionNals = {}; 17908 var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs 17909 17910 mdats.forEach(function (mdat, index) { 17911 var matchingTraf = trafs[index]; 17912 mdatTrafPairs.push({ 17913 mdat: mdat, 17914 traf: matchingTraf 17915 }); 17916 }); 17917 mdatTrafPairs.forEach(function (pair) { 17918 var mdat = pair.mdat; 17919 var traf = pair.traf; 17920 var tfhd = findBox$1(traf, ['tfhd']); // Exactly 1 tfhd per traf 17921 17922 var headerInfo = parseTfhd$1(tfhd[0]); 17923 var trackId = headerInfo.trackId; 17924 var tfdt = findBox$1(traf, ['tfdt']); // Either 0 or 1 tfdt per traf 17925 17926 var baseMediaDecodeTime = tfdt.length > 0 ? parseTfdt$1(tfdt[0]).baseMediaDecodeTime : 0; 17927 var truns = findBox$1(traf, ['trun']); 17928 var samples; 17929 var result; // Only parse video data for the chosen video track 17930 17931 if (videoTrackId === trackId && truns.length > 0) { 17932 samples = parseSamples(truns, baseMediaDecodeTime, headerInfo); 17933 result = findSeiNals(mdat, samples, trackId); 17934 17935 if (!captionNals[trackId]) { 17936 captionNals[trackId] = { 17937 seiNals: [], 17938 logs: [] 17939 }; 17940 } 17941 17942 captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals); 17943 captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs); 17944 } 17945 }); 17946 return captionNals; 17947 }; 17948 /** 17949 * Parses out inband captions from an MP4 container and returns 17950 * caption objects that can be used by WebVTT and the TextTrack API. 17951 * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue 17952 * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack 17953 * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first 17954 * 17955 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 17956 * @param {Number} trackId - The id of the video track to parse 17957 * @param {Number} timescale - The timescale for the video track from the init segment 17958 * 17959 * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks 17960 * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds 17961 * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds 17962 * @return {Object[]} parsedCaptions[].content - A list of individual caption segments 17963 * @return {String} parsedCaptions[].content.text - The visible content of the caption segment 17964 * @return {Number} parsedCaptions[].content.line - The line height from 1-15 for positioning of the caption segment 17965 * @return {Number} parsedCaptions[].content.position - The column indent percentage for cue positioning from 10-80 17966 **/ 17967 17968 17969 var parseEmbeddedCaptions = function (segment, trackId, timescale) { 17970 var captionNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 17971 17972 if (trackId === null) { 17973 return null; 17974 } 17975 17976 captionNals = parseCaptionNals(segment, trackId); 17977 var trackNals = captionNals[trackId] || {}; 17978 return { 17979 seiNals: trackNals.seiNals, 17980 logs: trackNals.logs, 17981 timescale: timescale 17982 }; 17983 }; 17984 /** 17985 * Converts SEI NALUs into captions that can be used by video.js 17986 **/ 17987 17988 17989 var CaptionParser = function () { 17990 var isInitialized = false; 17991 var captionStream; // Stores segments seen before trackId and timescale are set 17992 17993 var segmentCache; // Stores video track ID of the track being parsed 17994 17995 var trackId; // Stores the timescale of the track being parsed 17996 17997 var timescale; // Stores captions parsed so far 17998 17999 var parsedCaptions; // Stores whether we are receiving partial data or not 18000 18001 var parsingPartial; 18002 /** 18003 * A method to indicate whether a CaptionParser has been initalized 18004 * @returns {Boolean} 18005 **/ 18006 18007 this.isInitialized = function () { 18008 return isInitialized; 18009 }; 18010 /** 18011 * Initializes the underlying CaptionStream, SEI NAL parsing 18012 * and management, and caption collection 18013 **/ 18014 18015 18016 this.init = function (options) { 18017 captionStream = new CaptionStream(); 18018 isInitialized = true; 18019 parsingPartial = options ? options.isPartial : false; // Collect dispatched captions 18020 18021 captionStream.on('data', function (event) { 18022 // Convert to seconds in the source's timescale 18023 event.startTime = event.startPts / timescale; 18024 event.endTime = event.endPts / timescale; 18025 parsedCaptions.captions.push(event); 18026 parsedCaptions.captionStreams[event.stream] = true; 18027 }); 18028 captionStream.on('log', function (log) { 18029 parsedCaptions.logs.push(log); 18030 }); 18031 }; 18032 /** 18033 * Determines if a new video track will be selected 18034 * or if the timescale changed 18035 * @return {Boolean} 18036 **/ 18037 18038 18039 this.isNewInit = function (videoTrackIds, timescales) { 18040 if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) { 18041 return false; 18042 } 18043 18044 return trackId !== videoTrackIds[0] || timescale !== timescales[trackId]; 18045 }; 18046 /** 18047 * Parses out SEI captions and interacts with underlying 18048 * CaptionStream to return dispatched captions 18049 * 18050 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 18051 * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment 18052 * @param {Object.<Number, Number>} timescales - The timescales found in the init segment 18053 * @see parseEmbeddedCaptions 18054 * @see m2ts/caption-stream.js 18055 **/ 18056 18057 18058 this.parse = function (segment, videoTrackIds, timescales) { 18059 var parsedData; 18060 18061 if (!this.isInitialized()) { 18062 return null; // This is not likely to be a video segment 18063 } else if (!videoTrackIds || !timescales) { 18064 return null; 18065 } else if (this.isNewInit(videoTrackIds, timescales)) { 18066 // Use the first video track only as there is no 18067 // mechanism to switch to other video tracks 18068 trackId = videoTrackIds[0]; 18069 timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment 18070 // data until we have one. 18071 // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 18072 } else if (trackId === null || !timescale) { 18073 segmentCache.push(segment); 18074 return null; 18075 } // Now that a timescale and trackId is set, parse cached segments 18076 18077 18078 while (segmentCache.length > 0) { 18079 var cachedSegment = segmentCache.shift(); 18080 this.parse(cachedSegment, videoTrackIds, timescales); 18081 } 18082 18083 parsedData = parseEmbeddedCaptions(segment, trackId, timescale); 18084 18085 if (parsedData && parsedData.logs) { 18086 parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs); 18087 } 18088 18089 if (parsedData === null || !parsedData.seiNals) { 18090 if (parsedCaptions.logs.length) { 18091 return { 18092 logs: parsedCaptions.logs, 18093 captions: [], 18094 captionStreams: [] 18095 }; 18096 } 18097 18098 return null; 18099 } 18100 18101 this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched 18102 18103 this.flushStream(); 18104 return parsedCaptions; 18105 }; 18106 /** 18107 * Pushes SEI NALUs onto CaptionStream 18108 * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals` 18109 * Assumes that `parseCaptionNals` has been called first 18110 * @see m2ts/caption-stream.js 18111 **/ 18112 18113 18114 this.pushNals = function (nals) { 18115 if (!this.isInitialized() || !nals || nals.length === 0) { 18116 return null; 18117 } 18118 18119 nals.forEach(function (nal) { 18120 captionStream.push(nal); 18121 }); 18122 }; 18123 /** 18124 * Flushes underlying CaptionStream to dispatch processed, displayable captions 18125 * @see m2ts/caption-stream.js 18126 **/ 18127 18128 18129 this.flushStream = function () { 18130 if (!this.isInitialized()) { 18131 return null; 18132 } 18133 18134 if (!parsingPartial) { 18135 captionStream.flush(); 18136 } else { 18137 captionStream.partialFlush(); 18138 } 18139 }; 18140 /** 18141 * Reset caption buckets for new data 18142 **/ 18143 18144 18145 this.clearParsedCaptions = function () { 18146 parsedCaptions.captions = []; 18147 parsedCaptions.captionStreams = {}; 18148 parsedCaptions.logs = []; 18149 }; 18150 /** 18151 * Resets underlying CaptionStream 18152 * @see m2ts/caption-stream.js 18153 **/ 18154 18155 18156 this.resetCaptionStream = function () { 18157 if (!this.isInitialized()) { 18158 return null; 18159 } 18160 18161 captionStream.reset(); 18162 }; 18163 /** 18164 * Convenience method to clear all captions flushed from the 18165 * CaptionStream and still being parsed 18166 * @see m2ts/caption-stream.js 18167 **/ 18168 18169 18170 this.clearAllCaptions = function () { 18171 this.clearParsedCaptions(); 18172 this.resetCaptionStream(); 18173 }; 18174 /** 18175 * Reset caption parser 18176 **/ 18177 18178 18179 this.reset = function () { 18180 segmentCache = []; 18181 trackId = null; 18182 timescale = null; 18183 18184 if (!parsedCaptions) { 18185 parsedCaptions = { 18186 captions: [], 18187 // CC1, CC2, CC3, CC4 18188 captionStreams: {}, 18189 logs: [] 18190 }; 18191 } else { 18192 this.clearParsedCaptions(); 18193 } 18194 18195 this.resetCaptionStream(); 18196 }; 18197 18198 this.reset(); 18199 }; 18200 18201 var captionParser = CaptionParser; 18202 /** 18203 * Returns the first string in the data array ending with a null char '\0' 18204 * @param {UInt8} data 18205 * @returns the string with the null char 18206 */ 18207 18208 var uint8ToCString$1 = function (data) { 18209 var index = 0; 18210 var curChar = String.fromCharCode(data[index]); 18211 var retString = ''; 18212 18213 while (curChar !== '\0') { 18214 retString += curChar; 18215 index++; 18216 curChar = String.fromCharCode(data[index]); 18217 } // Add nullChar 18218 18219 18220 retString += curChar; 18221 return retString; 18222 }; 18223 18224 var string = { 18225 uint8ToCString: uint8ToCString$1 18226 }; 18227 var uint8ToCString = string.uint8ToCString; 18228 var getUint64$1 = numbers.getUint64; 18229 /** 18230 * Based on: ISO/IEC 23009 Section: 5.10.3.3 18231 * References: 18232 * https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format 18233 * https://aomediacodec.github.io/id3-emsg/ 18234 * 18235 * Takes emsg box data as a uint8 array and returns a emsg box object 18236 * @param {UInt8Array} boxData data from emsg box 18237 * @returns A parsed emsg box object 18238 */ 18239 18240 var parseEmsgBox = function (boxData) { 18241 // version + flags 18242 var offset = 4; 18243 var version = boxData[0]; 18244 var scheme_id_uri, value, timescale, presentation_time, presentation_time_delta, event_duration, id, message_data; 18245 18246 if (version === 0) { 18247 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 18248 offset += scheme_id_uri.length; 18249 value = uint8ToCString(boxData.subarray(offset)); 18250 offset += value.length; 18251 var dv = new DataView(boxData.buffer); 18252 timescale = dv.getUint32(offset); 18253 offset += 4; 18254 presentation_time_delta = dv.getUint32(offset); 18255 offset += 4; 18256 event_duration = dv.getUint32(offset); 18257 offset += 4; 18258 id = dv.getUint32(offset); 18259 offset += 4; 18260 } else if (version === 1) { 18261 var dv = new DataView(boxData.buffer); 18262 timescale = dv.getUint32(offset); 18263 offset += 4; 18264 presentation_time = getUint64$1(boxData.subarray(offset)); 18265 offset += 8; 18266 event_duration = dv.getUint32(offset); 18267 offset += 4; 18268 id = dv.getUint32(offset); 18269 offset += 4; 18270 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 18271 offset += scheme_id_uri.length; 18272 value = uint8ToCString(boxData.subarray(offset)); 18273 offset += value.length; 18274 } 18275 18276 message_data = new Uint8Array(boxData.subarray(offset, boxData.byteLength)); 18277 var emsgBox = { 18278 scheme_id_uri, 18279 value, 18280 // if timescale is undefined or 0 set to 1 18281 timescale: timescale ? timescale : 1, 18282 presentation_time, 18283 presentation_time_delta, 18284 event_duration, 18285 id, 18286 message_data 18287 }; 18288 return isValidEmsgBox(version, emsgBox) ? emsgBox : undefined; 18289 }; 18290 /** 18291 * Scales a presentation time or time delta with an offset with a provided timescale 18292 * @param {number} presentationTime 18293 * @param {number} timescale 18294 * @param {number} timeDelta 18295 * @param {number} offset 18296 * @returns the scaled time as a number 18297 */ 18298 18299 18300 var scaleTime = function (presentationTime, timescale, timeDelta, offset) { 18301 return presentationTime || presentationTime === 0 ? presentationTime / timescale : offset + timeDelta / timescale; 18302 }; 18303 /** 18304 * Checks the emsg box data for validity based on the version 18305 * @param {number} version of the emsg box to validate 18306 * @param {Object} emsg the emsg data to validate 18307 * @returns if the box is valid as a boolean 18308 */ 18309 18310 18311 var isValidEmsgBox = function (version, emsg) { 18312 var hasScheme = emsg.scheme_id_uri !== '\0'; 18313 var isValidV0Box = version === 0 && isDefined(emsg.presentation_time_delta) && hasScheme; 18314 var isValidV1Box = version === 1 && isDefined(emsg.presentation_time) && hasScheme; // Only valid versions of emsg are 0 and 1 18315 18316 return !(version > 1) && isValidV0Box || isValidV1Box; 18317 }; // Utility function to check if an object is defined 18318 18319 18320 var isDefined = function (data) { 18321 return data !== undefined || data !== null; 18322 }; 18323 18324 var emsg$1 = { 18325 parseEmsgBox: parseEmsgBox, 18326 scaleTime: scaleTime 18327 }; 18328 /** 18329 * mux.js 18330 * 18331 * Copyright (c) Brightcove 18332 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 18333 * 18334 * Utilities to detect basic properties and metadata about MP4s. 18335 */ 18336 18337 var toUnsigned = bin.toUnsigned; 18338 var toHexString = bin.toHexString; 18339 var findBox = findBox_1; 18340 var parseType$1 = parseType_1; 18341 var emsg = emsg$1; 18342 var parseTfhd = parseTfhd$2; 18343 var parseTrun = parseTrun$2; 18344 var parseTfdt = parseTfdt$2; 18345 var getUint64 = numbers.getUint64; 18346 var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks, getTimescaleFromMediaHeader, getEmsgID3; 18347 var window$1 = window_1; 18348 var parseId3Frames = parseId3.parseId3Frames; 18349 /** 18350 * Parses an MP4 initialization segment and extracts the timescale 18351 * values for any declared tracks. Timescale values indicate the
18352 * number of clock ticks per second to assume for time-based values 18353 * elsewhere in the MP4. 18354 * 18355 * To determine the start time of an MP4, you need two pieces of 18356 * information: the timescale unit and the earliest base media decode 18357 * time. Multiple timescales can be specified within an MP4 but the 18358 * base media decode time is always expressed in the timescale from 18359 * the media header box for the track: 18360 * ``` 18361 * moov > trak > mdia > mdhd.timescale 18362 * ``` 18363 * @param init {Uint8Array} the bytes of the init segment 18364 * @return {object} a hash of track ids to timescale values or null if 18365 * the init segment is malformed. 18366 */ 18367 18368 timescale = function (init) { 18369 var result = {}, 18370 traks = findBox(init, ['moov', 'trak']); // mdhd timescale 18371 18372 return traks.reduce(function (result, trak) { 18373 var tkhd, version, index, id, mdhd; 18374 tkhd = findBox(trak, ['tkhd'])[0]; 18375 18376 if (!tkhd) { 18377 return null; 18378 } 18379 18380 version = tkhd[0]; 18381 index = version === 0 ? 12 : 20; 18382 id = toUnsigned(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]); 18383 mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; 18384 18385 if (!mdhd) { 18386 return null; 18387 } 18388 18389 version = mdhd[0]; 18390 index = version === 0 ? 12 : 20; 18391 result[id] = toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 18392 return result; 18393 }, result); 18394 }; 18395 /** 18396 * Determine the base media decode start time, in seconds, for an MP4 18397 * fragment. If multiple fragments are specified, the earliest time is 18398 * returned. 18399 * 18400 * The base media decode time can be parsed from track fragment 18401 * metadata: 18402 * ``` 18403 * moof > traf > tfdt.baseMediaDecodeTime 18404 * ``` 18405 * It requires the timescale value from the mdhd to interpret. 18406 * 18407 * @param timescale {object} a hash of track ids to timescale values. 18408 * @return {number} the earliest base media decode start time for the 18409 * fragment, in seconds 18410 */ 18411 18412 18413 startTime = function (timescale, fragment) { 18414 var trafs; // we need info from two childrend of each track fragment box 18415 18416 trafs = findBox(fragment, ['moof', 'traf']); // determine the start times for each track 18417 18418 var lowestTime = trafs.reduce(function (acc, traf) { 18419 var tfhd = findBox(traf, ['tfhd'])[0]; // get the track id from the tfhd 18420 18421 var id = toUnsigned(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); // assume a 90kHz clock if no timescale was specified 18422 18423 var scale = timescale[id] || 90e3; // get the base media decode time from the tfdt 18424 18425 var tfdt = findBox(traf, ['tfdt'])[0]; 18426 var dv = new DataView(tfdt.buffer, tfdt.byteOffset, tfdt.byteLength); 18427 var baseTime; // version 1 is 64 bit 18428 18429 if (tfdt[0] === 1) { 18430 baseTime = getUint64(tfdt.subarray(4, 12)); 18431 } else { 18432 baseTime = dv.getUint32(4); 18433 }
vendor: 9,853 bytes, lines 18433-18703
18433 // convert base time to seconds if it is a valid number. 18434 18435 18436 let seconds; 18437 18438 if (typeof baseTime === 'bigint') { 18439 seconds = baseTime / window$1.BigInt(scale); 18440 } else if (typeof baseTime === 'number' && !isNaN(baseTime)) { 18441 seconds = baseTime / scale; 18442 } 18443 18444 if (seconds < Number.MAX_SAFE_INTEGER) { 18445 seconds = Number(seconds); 18446 } 18447 18448 if (seconds < acc) { 18449 acc = seconds; 18450 } 18451 18452 return acc; 18453 }, Infinity); 18454 return typeof lowestTime === 'bigint' || isFinite(lowestTime) ? lowestTime : 0; 18455 }; 18456 /** 18457 * Determine the composition start, in seconds, for an MP4 18458 * fragment. 18459 * 18460 * The composition start time of a fragment can be calculated using the base 18461 * media decode time, composition time offset, and timescale, as follows: 18462 * 18463 * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale 18464 * 18465 * All of the aforementioned information is contained within a media fragment's 18466 * `traf` box, except for timescale info, which comes from the initialization 18467 * segment, so a track id (also contained within a `traf`) is also necessary to 18468 * associate it with a timescale 18469 * 18470 * 18471 * @param timescales {object} - a hash of track ids to timescale values. 18472 * @param fragment {Unit8Array} - the bytes of a media segment 18473 * @return {number} the composition start time for the fragment, in seconds 18474 **/ 18475 18476 18477 compositionStartTime = function (timescales, fragment) { 18478 var trafBoxes = findBox(fragment, ['moof', 'traf']); 18479 var baseMediaDecodeTime = 0; 18480 var compositionTimeOffset = 0; 18481 var trackId; 18482 18483 if (trafBoxes && trafBoxes.length) { 18484 // The spec states that track run samples contained within a `traf` box are contiguous, but 18485 // it does not explicitly state whether the `traf` boxes themselves are contiguous. 18486 // We will assume that they are, so we only need the first to calculate start time. 18487 var tfhd = findBox(trafBoxes[0], ['tfhd'])[0]; 18488 var trun = findBox(trafBoxes[0], ['trun'])[0]; 18489 var tfdt = findBox(trafBoxes[0], ['tfdt'])[0]; 18490 18491 if (tfhd) { 18492 var parsedTfhd = parseTfhd(tfhd); 18493 trackId = parsedTfhd.trackId; 18494 } 18495 18496 if (tfdt) { 18497 var parsedTfdt = parseTfdt(tfdt); 18498 baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime; 18499 } 18500 18501 if (trun) { 18502 var parsedTrun = parseTrun(trun); 18503 18504 if (parsedTrun.samples && parsedTrun.samples.length) { 18505 compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0; 18506 } 18507 } 18508 } // Get timescale for this specific track. Assume a 90kHz clock if no timescale was 18509 // specified. 18510 18511 18512 var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds 18513 18514 if (typeof baseMediaDecodeTime === 'bigint') { 18515 compositionTimeOffset = window$1.BigInt(compositionTimeOffset); 18516 timescale = window$1.BigInt(timescale); 18517 } 18518 18519 var result = (baseMediaDecodeTime + compositionTimeOffset) / timescale; 18520 18521 if (typeof result === 'bigint' && result < Number.MAX_SAFE_INTEGER) { 18522 result = Number(result); 18523 } 18524 18525 return result; 18526 }; 18527 /** 18528 * Find the trackIds of the video tracks in this source. 18529 * Found by parsing the Handler Reference and Track Header Boxes: 18530 * moov > trak > mdia > hdlr 18531 * moov > trak > tkhd 18532 * 18533 * @param {Uint8Array} init - The bytes of the init segment for this source 18534 * @return {Number[]} A list of trackIds 18535 * 18536 * @see ISO-BMFF-12/2015, Section 8.4.3 18537 **/ 18538 18539 18540 getVideoTrackIds = function (init) { 18541 var traks = findBox(init, ['moov', 'trak']); 18542 var videoTrackIds = []; 18543 traks.forEach(function (trak) { 18544 var hdlrs = findBox(trak, ['mdia', 'hdlr']); 18545 var tkhds = findBox(trak, ['tkhd']); 18546 hdlrs.forEach(function (hdlr, index) { 18547 var handlerType = parseType$1(hdlr.subarray(8, 12)); 18548 var tkhd = tkhds[index]; 18549 var view; 18550 var version; 18551 var trackId; 18552 18553 if (handlerType === 'vide') { 18554 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 18555 version = view.getUint8(0); 18556 trackId = version === 0 ? view.getUint32(12) : view.getUint32(20); 18557 videoTrackIds.push(trackId); 18558 } 18559 }); 18560 }); 18561 return videoTrackIds; 18562 }; 18563 18564 getTimescaleFromMediaHeader = function (mdhd) { 18565 // mdhd is a FullBox, meaning it will have its own version as the first byte 18566 var version = mdhd[0]; 18567 var index = version === 0 ? 12 : 20; 18568 return toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 18569 }; 18570 /** 18571 * Get all the video, audio, and hint tracks from a non fragmented 18572 * mp4 segment 18573 */ 18574 18575 18576 getTracks = function (init) { 18577 var traks = findBox(init, ['moov', 'trak']); 18578 var tracks = []; 18579 traks.forEach(function (trak) { 18580 var track = {}; 18581 var tkhd = findBox(trak, ['tkhd'])[0]; 18582 var view, tkhdVersion; // id 18583 18584 if (tkhd) { 18585 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 18586 tkhdVersion = view.getUint8(0); 18587 track.id = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20); 18588 } 18589 18590 var hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; // type 18591 18592 if (hdlr) { 18593 var type = parseType$1(hdlr.subarray(8, 12)); 18594 18595 if (type === 'vide') { 18596 track.type = 'video'; 18597 } else if (type === 'soun') { 18598 track.type = 'audio'; 18599 } else { 18600 track.type = type; 18601 } 18602 } // codec 18603 18604 18605 var stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 18606 18607 if (stsd) { 18608 var sampleDescriptions = stsd.subarray(8); // gives the codec type string 18609 18610 track.codec = parseType$1(sampleDescriptions.subarray(4, 8)); 18611 var codecBox = findBox(sampleDescriptions, [track.codec])[0]; 18612 var codecConfig, codecConfigType; 18613 18614 if (codecBox) { 18615 // https://tools.ietf.org/html/rfc6381#section-3.3 18616 if (/^[asm]vc[1-9]$/i.test(track.codec)) { 18617 // we don't need anything but the "config" parameter of the 18618 // avc1 codecBox 18619 codecConfig = codecBox.subarray(78); 18620 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 18621 18622 if (codecConfigType === 'avcC' && codecConfig.length > 11) { 18623 track.codec += '.'; // left padded with zeroes for single digit hex 18624 // profile idc 18625 18626 track.codec += toHexString(codecConfig[9]); // the byte containing the constraint_set flags 18627 18628 track.codec += toHexString(codecConfig[10]); // level idc 18629 18630 track.codec += toHexString(codecConfig[11]); 18631 } else { 18632 // TODO: show a warning that we couldn't parse the codec 18633 // and are using the default 18634 track.codec = 'avc1.4d400d'; 18635 } 18636 } else if (/^mp4[a,v]$/i.test(track.codec)) { 18637 // we do not need anything but the streamDescriptor of the mp4a codecBox 18638 codecConfig = codecBox.subarray(28); 18639 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 18640 18641 if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) { 18642 track.codec += '.' + toHexString(codecConfig[19]); // this value is only a single digit 18643 18644 track.codec += '.' + toHexString(codecConfig[20] >>> 2 & 0x3f).replace(/^0/, ''); 18645 } else { 18646 // TODO: show a warning that we couldn't parse the codec 18647 // and are using the default 18648 track.codec = 'mp4a.40.2'; 18649 } 18650 } else { 18651 // flac, opus, etc 18652 track.codec = track.codec.toLowerCase(); 18653 } 18654 } 18655 } 18656 18657 var mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; 18658 18659 if (mdhd) { 18660 track.timescale = getTimescaleFromMediaHeader(mdhd); 18661 } 18662 18663 tracks.push(track); 18664 }); 18665 return tracks; 18666 }; 18667 /** 18668 * Returns an array of emsg ID3 data from the provided segmentData. 18669 * An offset can also be provided as the Latest Arrival Time to calculate 18670 * the Event Start Time of v0 EMSG boxes. 18671 * See: https://dashif-documents.azurewebsites.net/Events/master/event.html#Inband-event-timing 18672 * 18673 * @param {Uint8Array} segmentData the segment byte array. 18674 * @param {number} offset the segment start time or Latest Arrival Time, 18675 * @return {Object[]} an array of ID3 parsed from EMSG boxes 18676 */ 18677 18678 18679 getEmsgID3 = function (segmentData, offset = 0) { 18680 var emsgBoxes = findBox(segmentData, ['emsg']); 18681 return emsgBoxes.map(data => { 18682 var parsedBox = emsg.parseEmsgBox(new Uint8Array(data)); 18683 var parsedId3Frames = parseId3Frames(parsedBox.message_data); 18684 return { 18685 cueTime: emsg.scaleTime(parsedBox.presentation_time, parsedBox.timescale, parsedBox.presentation_time_delta, offset), 18686 duration: emsg.scaleTime(parsedBox.event_duration, parsedBox.timescale), 18687 frames: parsedId3Frames 18688 }; 18689 }); 18690 }; 18691 18692 var probe$2 = { 18693 // export mp4 inspector's findBox and parseType for backwards compatibility 18694 findBox: findBox, 18695 parseType: parseType$1, 18696 timescale: timescale, 18697 startTime: startTime, 18698 compositionStartTime: compositionStartTime, 18699 videoTrackIds: getVideoTrackIds, 18700 tracks: getTracks, 18701 getTimescaleFromMediaHeader: getTimescaleFromMediaHeader, 18702 getEmsgID3: getEmsgID3 18703 }
vendor: 38,274 bytes, lines 18703-19912
18703; 18704 /** 18705 * mux.js 18706 * 18707 * Copyright (c) Brightcove 18708 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 18709 * 18710 * Utilities to detect basic properties and metadata about TS Segments. 18711 */ 18712 18713 var StreamTypes$1 = streamTypes; 18714 18715 var parsePid = function (packet) { 18716 var pid = packet[1] & 0x1f; 18717 pid <<= 8; 18718 pid |= packet[2]; 18719 return pid; 18720 }; 18721 18722 var parsePayloadUnitStartIndicator = function (packet) { 18723 return !!(packet[1] & 0x40); 18724 }; 18725 18726 var parseAdaptionField = function (packet) { 18727 var offset = 0; // if an adaption field is present, its length is specified by the 18728 // fifth byte of the TS packet header. The adaptation field is 18729 // used to add stuffing to PES packets that don't fill a complete 18730 // TS packet, and to specify some forms of timing and control data 18731 // that we do not currently use. 18732 18733 if ((packet[3] & 0x30) >>> 4 > 0x01) { 18734 offset += packet[4] + 1; 18735 } 18736 18737 return offset; 18738 }; 18739 18740 var parseType = function (packet, pmtPid) { 18741 var pid = parsePid(packet); 18742 18743 if (pid === 0) { 18744 return 'pat'; 18745 } else if (pid === pmtPid) { 18746 return 'pmt'; 18747 } else if (pmtPid) { 18748 return 'pes'; 18749 } 18750 18751 return null; 18752 }; 18753 18754 var parsePat = function (packet) { 18755 var pusi = parsePayloadUnitStartIndicator(packet); 18756 var offset = 4 + parseAdaptionField(packet); 18757 18758 if (pusi) { 18759 offset += packet[offset] + 1; 18760 } 18761 18762 return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; 18763 }; 18764 18765 var parsePmt = function (packet) { 18766 var programMapTable = {}; 18767 var pusi = parsePayloadUnitStartIndicator(packet); 18768 var payloadOffset = 4 + parseAdaptionField(packet); 18769 18770 if (pusi) { 18771 payloadOffset += packet[payloadOffset] + 1; 18772 } // PMTs can be sent ahead of the time when they should actually 18773 // take effect. We don't believe this should ever be the case 18774 // for HLS but we'll ignore "forward" PMT declarations if we see 18775 // them. Future PMT declarations have the current_next_indicator 18776 // set to zero. 18777 18778 18779 if (!(packet[payloadOffset + 5] & 0x01)) { 18780 return; 18781 } 18782 18783 var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section 18784 18785 sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; 18786 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 18787 // long the program info descriptors are 18788 18789 programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table 18790 18791 var offset = 12 + programInfoLength; 18792 18793 while (offset < tableEnd) { 18794 var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type 18795 18796 programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; // move to the next table entry 18797 // skip past the elementary stream descriptors, if present 18798 18799 offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; 18800 } 18801 18802 return programMapTable; 18803 }; 18804 18805 var parsePesType = function (packet, programMapTable) { 18806 var pid = parsePid(packet); 18807 var type = programMapTable[pid]; 18808 18809 switch (type) { 18810 case StreamTypes$1.H264_STREAM_TYPE: 18811 return 'video'; 18812 18813 case StreamTypes$1.ADTS_STREAM_TYPE: 18814 return 'audio'; 18815 18816 case StreamTypes$1.METADATA_STREAM_TYPE: 18817 return 'timed-metadata'; 18818 18819 default: 18820 return null; 18821 } 18822 }; 18823 18824 var parsePesTime = function (packet) { 18825 var pusi = parsePayloadUnitStartIndicator(packet); 18826 18827 if (!pusi) { 18828 return null; 18829 } 18830 18831 var offset = 4 + parseAdaptionField(packet); 18832 18833 if (offset >= packet.byteLength) { 18834 // From the H 222.0 MPEG-TS spec 18835 // "For transport stream packets carrying PES packets, stuffing is needed when there 18836 // is insufficient PES packet data to completely fill the transport stream packet 18837 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 18838 // the sum of the lengths of the data elements in it, so that the payload bytes 18839 // remaining after the adaptation field exactly accommodates the available PES packet 18840 // data." 18841 // 18842 // If the offset is >= the length of the packet, then the packet contains no data 18843 // and instead is just adaption field stuffing bytes 18844 return null; 18845 } 18846 18847 var pes = null; 18848 var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value 18849 // and a DTS value. Determine what combination of values is 18850 // available to work with. 18851 18852 ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 18853 // performs all bitwise operations on 32-bit integers but javascript 18854 // supports a much greater range (52-bits) of integer using standard 18855 // mathematical operations. 18856 // We construct a 31-bit value using bitwise operators over the 31 18857 // most significant bits and then multiply by 4 (equal to a left-shift 18858 // of 2) before we add the final 2 least significant bits of the 18859 // timestamp (equal to an OR.) 18860 18861 if (ptsDtsFlags & 0xC0) { 18862 pes = {}; // the PTS and DTS are not written out directly. For information 18863 // on how they are encoded, see 18864 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 18865 18866 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; 18867 pes.pts *= 4; // Left shift by 2 18868 18869 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 18870 18871 pes.dts = pes.pts; 18872 18873 if (ptsDtsFlags & 0x40) { 18874 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; 18875 pes.dts *= 4; // Left shift by 2 18876 18877 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 18878 } 18879 } 18880 18881 return pes; 18882 }; 18883 18884 var parseNalUnitType = function (type) { 18885 switch (type) { 18886 case 0x05: 18887 return 'slice_layer_without_partitioning_rbsp_idr'; 18888 18889 case 0x06: 18890 return 'sei_rbsp'; 18891 18892 case 0x07: 18893 return 'seq_parameter_set_rbsp'; 18894 18895 case 0x08: 18896 return 'pic_parameter_set_rbsp'; 18897 18898 case 0x09: 18899 return 'access_unit_delimiter_rbsp'; 18900 18901 default: 18902 return null; 18903 } 18904 }; 18905 18906 var videoPacketContainsKeyFrame = function (packet) { 18907 var offset = 4 + parseAdaptionField(packet); 18908 var frameBuffer = packet.subarray(offset); 18909 var frameI = 0; 18910 var frameSyncPoint = 0; 18911 var foundKeyFrame = false; 18912 var nalType; // advance the sync point to a NAL start, if necessary 18913 18914 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 18915 if (frameBuffer[frameSyncPoint + 2] === 1) { 18916 // the sync point is properly aligned 18917 frameI = frameSyncPoint + 5; 18918 break; 18919 } 18920 } 18921 18922 while (frameI < frameBuffer.byteLength) { 18923 // look at the current byte to determine if we've hit the end of 18924 // a NAL unit boundary 18925 switch (frameBuffer[frameI]) { 18926 case 0: 18927 // skip past non-sync sequences 18928 if (frameBuffer[frameI - 1] !== 0) { 18929 frameI += 2; 18930 break; 18931 } else if (frameBuffer[frameI - 2] !== 0) { 18932 frameI++; 18933 break; 18934 } 18935 18936 if (frameSyncPoint + 3 !== frameI - 2) { 18937 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 18938 18939 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 18940 foundKeyFrame = true; 18941 } 18942 } // drop trailing zeroes 18943 18944 18945 do { 18946 frameI++; 18947 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 18948 18949 frameSyncPoint = frameI - 2; 18950 frameI += 3; 18951 break; 18952 18953 case 1: 18954 // skip past non-sync sequences 18955 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 18956 frameI += 3; 18957 break; 18958 } 18959 18960 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 18961 18962 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 18963 foundKeyFrame = true; 18964 } 18965 18966 frameSyncPoint = frameI - 2; 18967 frameI += 3; 18968 break; 18969 18970 default: 18971 // the current byte isn't a one or zero, so it cannot be part 18972 // of a sync sequence 18973 frameI += 3; 18974 break; 18975 } 18976 } 18977 18978 frameBuffer = frameBuffer.subarray(frameSyncPoint); 18979 frameI -= frameSyncPoint; 18980 frameSyncPoint = 0; // parse the final nal 18981 18982 if (frameBuffer && frameBuffer.byteLength > 3) { 18983 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 18984 18985 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 18986 foundKeyFrame = true; 18987 } 18988 } 18989 18990 return foundKeyFrame; 18991 }; 18992 18993 var probe$1 = { 18994 parseType: parseType, 18995 parsePat: parsePat, 18996 parsePmt: parsePmt, 18997 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 18998 parsePesType: parsePesType, 18999 parsePesTime: parsePesTime, 19000 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame 19001 }; 19002 /** 19003 * mux.js 19004 * 19005 * Copyright (c) Brightcove 19006 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 19007 * 19008 * Parse mpeg2 transport stream packets to extract basic timing information 19009 */ 19010 19011 var StreamTypes = streamTypes; 19012 var handleRollover = timestampRolloverStream.handleRollover; 19013 var probe = {}; 19014 probe.ts = probe$1; 19015 probe.aac = utils; 19016 var ONE_SECOND_IN_TS = clock$2.ONE_SECOND_IN_TS; 19017 var MP2T_PACKET_LENGTH = 188, 19018 // bytes 19019 SYNC_BYTE = 0x47; 19020 /** 19021 * walks through segment data looking for pat and pmt packets to parse out 19022 * program map table information 19023 */ 19024 19025 var parsePsi_ = function (bytes, pmt) { 19026 var startIndex = 0, 19027 endIndex = MP2T_PACKET_LENGTH, 19028 packet, 19029 type; 19030 19031 while (endIndex < bytes.byteLength) { 19032 // Look for a pair of start and end sync bytes in the data.. 19033 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 19034 // We found a packet 19035 packet = bytes.subarray(startIndex, endIndex); 19036 type = probe.ts.parseType(packet, pmt.pid); 19037 19038 switch (type) { 19039 case 'pat': 19040 pmt.pid = probe.ts.parsePat(packet); 19041 break; 19042 19043 case 'pmt': 19044 var table = probe.ts.parsePmt(packet); 19045 pmt.table = pmt.table || {}; 19046 Object.keys(table).forEach(function (key) { 19047 pmt.table[key] = table[key]; 19048 }); 19049 break; 19050 } 19051 19052 startIndex += MP2T_PACKET_LENGTH; 19053 endIndex += MP2T_PACKET_LENGTH; 19054 continue; 19055 } // If we get here, we have somehow become de-synchronized and we need to step 19056 // forward one byte at a time until we find a pair of sync bytes that denote 19057 // a packet 19058 19059 19060 startIndex++; 19061 endIndex++; 19062 } 19063 }; 19064 /** 19065 * walks through the segment data from the start and end to get timing information 19066 * for the first and last audio pes packets 19067 */ 19068 19069 19070 var parseAudioPes_ = function (bytes, pmt, result) { 19071 var startIndex = 0, 19072 endIndex = MP2T_PACKET_LENGTH, 19073 packet, 19074 type, 19075 pesType, 19076 pusi, 19077 parsed; 19078 var endLoop = false; // Start walking from start of segment to get first audio packet 19079 19080 while (endIndex <= bytes.byteLength) { 19081 // Look for a pair of start and end sync bytes in the data.. 19082 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 19083 // We found a packet 19084 packet = bytes.subarray(startIndex, endIndex); 19085 type = probe.ts.parseType(packet, pmt.pid); 19086 19087 switch (type) { 19088 case 'pes': 19089 pesType = probe.ts.parsePesType(packet, pmt.table); 19090 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 19091 19092 if (pesType === 'audio' && pusi) { 19093 parsed = probe.ts.parsePesTime(packet); 19094 19095 if (parsed) { 19096 parsed.type = 'audio'; 19097 result.audio.push(parsed); 19098 endLoop = true; 19099 } 19100 } 19101 19102 break; 19103 } 19104 19105 if (endLoop) { 19106 break; 19107 } 19108 19109 startIndex += MP2T_PACKET_LENGTH; 19110 endIndex += MP2T_PACKET_LENGTH; 19111 continue; 19112 } // If we get here, we have somehow become de-synchronized and we need to step 19113 // forward one byte at a time until we find a pair of sync bytes that denote 19114 // a packet 19115 19116 19117 startIndex++; 19118 endIndex++; 19119 } // Start walking from end of segment to get last audio packet 19120 19121 19122 endIndex = bytes.byteLength; 19123 startIndex = endIndex - MP2T_PACKET_LENGTH; 19124 endLoop = false; 19125 19126 while (startIndex >= 0) { 19127 // Look for a pair of start and end sync bytes in the data.. 19128 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 19129 // We found a packet 19130 packet = bytes.subarray(startIndex, endIndex); 19131 type = probe.ts.parseType(packet, pmt.pid); 19132 19133 switch (type) { 19134 case 'pes': 19135 pesType = probe.ts.parsePesType(packet, pmt.table); 19136 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 19137 19138 if (pesType === 'audio' && pusi) { 19139 parsed = probe.ts.parsePesTime(packet); 19140 19141 if (parsed) { 19142 parsed.type = 'audio'; 19143 result.audio.push(parsed); 19144 endLoop = true; 19145 } 19146 } 19147 19148 break; 19149 } 19150 19151 if (endLoop) { 19152 break; 19153 } 19154 19155 startIndex -= MP2T_PACKET_LENGTH; 19156 endIndex -= MP2T_PACKET_LENGTH; 19157 continue; 19158 } // If we get here, we have somehow become de-synchronized and we need to step 19159 // forward one byte at a time until we find a pair of sync bytes that denote 19160 // a packet 19161 19162 19163 startIndex--; 19164 endIndex--; 19165 } 19166 }; 19167 /** 19168 * walks through the segment data from the start and end to get timing information 19169 * for the first and last video pes packets as well as timing information for the first 19170 * key frame. 19171 */ 19172 19173 19174 var parseVideoPes_ = function (bytes, pmt, result) { 19175 var startIndex = 0, 19176 endIndex = MP2T_PACKET_LENGTH, 19177 packet, 19178 type, 19179 pesType, 19180 pusi, 19181 parsed, 19182 frame, 19183 i, 19184 pes; 19185 var endLoop = false; 19186 var currentFrame = { 19187 data: [], 19188 size: 0 19189 }; // Start walking from start of segment to get first video packet 19190 19191 while (endIndex < bytes.byteLength) { 19192 // Look for a pair of start and end sync bytes in the data.. 19193 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 19194 // We found a packet 19195 packet = bytes.subarray(startIndex, endIndex); 19196 type = probe.ts.parseType(packet, pmt.pid); 19197 19198 switch (type) { 19199 case 'pes': 19200 pesType = probe.ts.parsePesType(packet, pmt.table); 19201 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 19202 19203 if (pesType === 'video') { 19204 if (pusi && !endLoop) { 19205 parsed = probe.ts.parsePesTime(packet); 19206 19207 if (parsed) { 19208 parsed.type = 'video'; 19209 result.video.push(parsed); 19210 endLoop = true; 19211 } 19212 } 19213 19214 if (!result.firstKeyFrame) { 19215 if (pusi) { 19216 if (currentFrame.size !== 0) { 19217 frame = new Uint8Array(currentFrame.size); 19218 i = 0; 19219 19220 while (currentFrame.data.length) { 19221 pes = currentFrame.data.shift(); 19222 frame.set(pes, i); 19223 i += pes.byteLength; 19224 } 19225 19226 if (probe.ts.videoPacketContainsKeyFrame(frame)) { 19227 var firstKeyFrame = probe.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting 19228 // the keyframe seems to work fine with HLS playback 19229 // and definitely preferable to a crash with TypeError... 19230 19231 if (firstKeyFrame) { 19232 result.firstKeyFrame = firstKeyFrame; 19233 result.firstKeyFrame.type = 'video'; 19234 } else { 19235 // eslint-disable-next-line 19236 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.'); 19237 } 19238 } 19239 19240 currentFrame.size = 0; 19241 } 19242 } 19243 19244 currentFrame.data.push(packet); 19245 currentFrame.size += packet.byteLength; 19246 } 19247 } 19248 19249 break; 19250 } 19251 19252 if (endLoop && result.firstKeyFrame) { 19253 break; 19254 } 19255 19256 startIndex += MP2T_PACKET_LENGTH; 19257 endIndex += MP2T_PACKET_LENGTH; 19258 continue; 19259 } // If we get here, we have somehow become de-synchronized and we need to step 19260 // forward one byte at a time until we find a pair of sync bytes that denote 19261 // a packet 19262 19263 19264 startIndex++; 19265 endIndex++; 19266 } // Start walking from end of segment to get last video packet 19267 19268 19269 endIndex = bytes.byteLength; 19270 startIndex = endIndex - MP2T_PACKET_LENGTH; 19271 endLoop = false; 19272 19273 while (startIndex >= 0) { 19274 // Look for a pair of start and end sync bytes in the data.. 19275 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 19276 // We found a packet 19277 packet = bytes.subarray(startIndex, endIndex); 19278 type = probe.ts.parseType(packet, pmt.pid); 19279 19280 switch (type) { 19281 case 'pes': 19282 pesType = probe.ts.parsePesType(packet, pmt.table); 19283 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 19284 19285 if (pesType === 'video' && pusi) { 19286 parsed = probe.ts.parsePesTime(packet); 19287 19288 if (parsed) { 19289 parsed.type = 'video'; 19290 result.video.push(parsed); 19291 endLoop = true; 19292 } 19293 } 19294 19295 break; 19296 } 19297 19298 if (endLoop) { 19299 break; 19300 } 19301 19302 startIndex -= MP2T_PACKET_LENGTH; 19303 endIndex -= MP2T_PACKET_LENGTH; 19304 continue; 19305 } // If we get here, we have somehow become de-synchronized and we need to step 19306 // forward one byte at a time until we find a pair of sync bytes that denote 19307 // a packet 19308 19309 19310 startIndex--; 19311 endIndex--; 19312 } 19313 }; 19314 /** 19315 * Adjusts the timestamp information for the segment to account for 19316 * rollover and convert to seconds based on pes packet timescale (90khz clock) 19317 */ 19318 19319 19320 var adjustTimestamp_ = function (segmentInfo, baseTimestamp) { 19321 if (segmentInfo.audio && segmentInfo.audio.length) { 19322 var audioBaseTimestamp = baseTimestamp; 19323 19324 if (typeof audioBaseTimestamp === 'undefined' || isNaN(audioBaseTimestamp)) { 19325 audioBaseTimestamp = segmentInfo.audio[0].dts; 19326 } 19327 19328 segmentInfo.audio.forEach(function (info) { 19329 info.dts = handleRollover(info.dts, audioBaseTimestamp); 19330 info.pts = handleRollover(info.pts, audioBaseTimestamp); // time in seconds 19331 19332 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 19333 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 19334 }); 19335 } 19336 19337 if (segmentInfo.video && segmentInfo.video.length) { 19338 var videoBaseTimestamp = baseTimestamp; 19339 19340 if (typeof videoBaseTimestamp === 'undefined' || isNaN(videoBaseTimestamp)) { 19341 videoBaseTimestamp = segmentInfo.video[0].dts; 19342 } 19343 19344 segmentInfo.video.forEach(function (info) { 19345 info.dts = handleRollover(info.dts, videoBaseTimestamp); 19346 info.pts = handleRollover(info.pts, videoBaseTimestamp); // time in seconds 19347 19348 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 19349 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 19350 }); 19351 19352 if (segmentInfo.firstKeyFrame) { 19353 var frame = segmentInfo.firstKeyFrame; 19354 frame.dts = handleRollover(frame.dts, videoBaseTimestamp); 19355 frame.pts = handleRollover(frame.pts, videoBaseTimestamp); // time in seconds 19356 19357 frame.dtsTime = frame.dts / ONE_SECOND_IN_TS; 19358 frame.ptsTime = frame.pts / ONE_SECOND_IN_TS; 19359 } 19360 } 19361 }; 19362 /** 19363 * inspects the aac data stream for start and end time information 19364 */ 19365 19366 19367 var inspectAac_ = function (bytes) { 19368 var endLoop = false, 19369 audioCount = 0, 19370 sampleRate = null, 19371 timestamp = null, 19372 frameSize = 0, 19373 byteIndex = 0, 19374 packet; 19375 19376 while (bytes.length - byteIndex >= 3) { 19377 var type = probe.aac.parseType(bytes, byteIndex); 19378 19379 switch (type) { 19380 case 'timed-metadata': 19381 // Exit early because we don't have enough to parse 19382 // the ID3 tag header 19383 if (bytes.length - byteIndex < 10) { 19384 endLoop = true; 19385 break; 19386 } 19387 19388 frameSize = probe.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 19389 // to emit a full packet 19390 19391 if (frameSize > bytes.length) { 19392 endLoop = true; 19393 break; 19394 } 19395 19396 if (timestamp === null) { 19397 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 19398 timestamp = probe.aac.parseAacTimestamp(packet); 19399 } 19400 19401 byteIndex += frameSize; 19402 break; 19403 19404 case 'audio': 19405 // Exit early because we don't have enough to parse 19406 // the ADTS frame header 19407 if (bytes.length - byteIndex < 7) { 19408 endLoop = true; 19409 break; 19410 } 19411 19412 frameSize = probe.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 19413 // to emit a full packet 19414 19415 if (frameSize > bytes.length) { 19416 endLoop = true; 19417 break; 19418 } 19419 19420 if (sampleRate === null) { 19421 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 19422 sampleRate = probe.aac.parseSampleRate(packet); 19423 } 19424 19425 audioCount++; 19426 byteIndex += frameSize; 19427 break; 19428 19429 default: 19430 byteIndex++; 19431 break; 19432 } 19433 19434 if (endLoop) { 19435 return null; 19436 } 19437 } 19438 19439 if (sampleRate === null || timestamp === null) { 19440 return null; 19441 } 19442 19443 var audioTimescale = ONE_SECOND_IN_TS / sampleRate; 19444 var result = { 19445 audio: [{ 19446 type: 'audio', 19447 dts: timestamp, 19448 pts: timestamp 19449 }, { 19450 type: 'audio', 19451 dts: timestamp + audioCount * 1024 * audioTimescale, 19452 pts: timestamp + audioCount * 1024 * audioTimescale 19453 }] 19454 }; 19455 return result; 19456 }; 19457 /** 19458 * inspects the transport stream segment data for start and end time information 19459 * of the audio and video tracks (when present) as well as the first key frame's 19460 * start time. 19461 */ 19462 19463 19464 var inspectTs_ = function (bytes) { 19465 var pmt = { 19466 pid: null, 19467 table: null 19468 }; 19469 var result = {}; 19470 parsePsi_(bytes, pmt); 19471 19472 for (var pid in pmt.table) { 19473 if (pmt.table.hasOwnProperty(pid)) { 19474 var type = pmt.table[pid]; 19475 19476 switch (type) { 19477 case StreamTypes.H264_STREAM_TYPE: 19478 result.video = []; 19479 parseVideoPes_(bytes, pmt, result); 19480 19481 if (result.video.length === 0) { 19482 delete result.video; 19483 } 19484 19485 break; 19486 19487 case StreamTypes.ADTS_STREAM_TYPE: 19488 result.audio = []; 19489 parseAudioPes_(bytes, pmt, result); 19490 19491 if (result.audio.length === 0) { 19492 delete result.audio; 19493 } 19494 19495 break; 19496 } 19497 } 19498 } 19499 19500 return result; 19501 }; 19502 /** 19503 * Inspects segment byte data and returns an object with start and end timing information 19504 * 19505 * @param {Uint8Array} bytes The segment byte data 19506 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 19507 * timestamps for rollover. This value must be in 90khz clock. 19508 * @return {Object} Object containing start and end frame timing info of segment. 19509 */ 19510 19511 19512 var inspect = function (bytes, baseTimestamp) { 19513 var isAacData = probe.aac.isLikelyAacData(bytes); 19514 var result; 19515 19516 if (isAacData) { 19517 result = inspectAac_(bytes); 19518 } else { 19519 result = inspectTs_(bytes); 19520 } 19521 19522 if (!result || !result.audio && !result.video) { 19523 return null; 19524 } 19525 19526 adjustTimestamp_(result, baseTimestamp); 19527 return result; 19528 }; 19529 19530 var tsInspector = { 19531 inspect: inspect, 19532 parseAudioPes_: parseAudioPes_ 19533 }; 19534 /* global self */ 19535 19536 /** 19537 * Re-emits transmuxer events by converting them into messages to the 19538 * world outside the worker. 19539 * 19540 * @param {Object} transmuxer the transmuxer to wire events on 19541 * @private 19542 */ 19543 19544 const wireTransmuxerEvents = function (self, transmuxer) { 19545 transmuxer.on('data', function (segment) { 19546 // transfer ownership of the underlying ArrayBuffer 19547 // instead of doing a copy to save memory 19548 // ArrayBuffers are transferable but generic TypedArrays are not 19549 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 19550 const initArray = segment.initSegment; 19551 segment.initSegment = { 19552 data: initArray.buffer, 19553 byteOffset: initArray.byteOffset, 19554 byteLength: initArray.byteLength 19555 }; 19556 const typedArray = segment.data; 19557 segment.data = typedArray.buffer; 19558 self.postMessage({ 19559 action: 'data', 19560 segment, 19561 byteOffset: typedArray.byteOffset, 19562 byteLength: typedArray.byteLength 19563 }, [segment.data]); 19564 }); 19565 transmuxer.on('done', function (data) { 19566 self.postMessage({ 19567 action: 'done' 19568 }); 19569 }); 19570 transmuxer.on('gopInfo', function (gopInfo) { 19571 self.postMessage({ 19572 action: 'gopInfo', 19573 gopInfo 19574 }); 19575 }); 19576 transmuxer.on('videoSegmentTimingInfo', function (timingInfo) { 19577 const videoSegmentTimingInfo = { 19578 start: { 19579 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 19580 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts) 19581 }, 19582 end: { 19583 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 19584 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts) 19585 }, 19586 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime) 19587 }; 19588 19589 if (timingInfo.prependedContentDuration) { 19590 videoSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration); 19591 } 19592 19593 self.postMessage({ 19594 action: 'videoSegmentTimingInfo', 19595 videoSegmentTimingInfo 19596 }); 19597 }); 19598 transmuxer.on('audioSegmentTimingInfo', function (timingInfo) { 19599 // Note that all times for [audio/video]SegmentTimingInfo events are in video clock 19600 const audioSegmentTimingInfo = { 19601 start: { 19602 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 19603 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts) 19604 }, 19605 end: { 19606 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 19607 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts) 19608 }, 19609 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime) 19610 }; 19611 19612 if (timingInfo.prependedContentDuration) { 19613 audioSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration); 19614 } 19615 19616 self.postMessage({ 19617 action: 'audioSegmentTimingInfo', 19618 audioSegmentTimingInfo 19619 }); 19620 }); 19621 transmuxer.on('id3Frame', function (id3Frame) { 19622 self.postMessage({ 19623 action: 'id3Frame', 19624 id3Frame 19625 }); 19626 }); 19627 transmuxer.on('caption', function (caption) { 19628 self.postMessage({ 19629 action: 'caption', 19630 caption 19631 }); 19632 }); 19633 transmuxer.on('trackinfo', function (trackInfo) { 19634 self.postMessage({ 19635 action: 'trackinfo', 19636 trackInfo 19637 }); 19638 }); 19639 transmuxer.on('audioTimingInfo', function (audioTimingInfo) { 19640 // convert to video TS since we prioritize video time over audio 19641 self.postMessage({ 19642 action: 'audioTimingInfo', 19643 audioTimingInfo: { 19644 start: clock$2.videoTsToSeconds(audioTimingInfo.start), 19645 end: clock$2.videoTsToSeconds(audioTimingInfo.end) 19646 } 19647 }); 19648 }); 19649 transmuxer.on('videoTimingInfo', function (videoTimingInfo) { 19650 self.postMessage({ 19651 action: 'videoTimingInfo', 19652 videoTimingInfo: { 19653 start: clock$2.videoTsToSeconds(videoTimingInfo.start), 19654 end: clock$2.videoTsToSeconds(videoTimingInfo.end) 19655 } 19656 }); 19657 }); 19658 transmuxer.on('log', function (log) { 19659 self.postMessage({ 19660 action: 'log', 19661 log 19662 }); 19663 }); 19664 }; 19665 /** 19666 * All incoming messages route through this hash. If no function exists 19667 * to handle an incoming message, then we ignore the message. 19668 * 19669 * @class MessageHandlers 19670 * @param {Object} options the options to initialize with 19671 */ 19672 19673 19674 class MessageHandlers { 19675 constructor(self, options) { 19676 this.options = options || {}; 19677 this.self = self; 19678 this.init(); 19679 } 19680 /** 19681 * initialize our web worker and wire all the events. 19682 */ 19683 19684 19685 init() { 19686 if (this.transmuxer) { 19687 this.transmuxer.dispose(); 19688 } 19689 19690 this.transmuxer = new transmuxer.Transmuxer(this.options); 19691 wireTransmuxerEvents(this.self, this.transmuxer); 19692 } 19693 19694 pushMp4Captions(data) { 19695 if (!this.captionParser) { 19696 this.captionParser = new captionParser(); 19697 this.captionParser.init(); 19698 } 19699 19700 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 19701 const parsed = this.captionParser.parse(segment, data.trackIds, data.timescales); 19702 this.self.postMessage({ 19703 action: 'mp4Captions', 19704 captions: parsed && parsed.captions || [], 19705 logs: parsed && parsed.logs || [], 19706 data: segment.buffer 19707 }, [segment.buffer]); 19708 } 19709 19710 probeMp4StartTime({ 19711 timescales, 19712 data 19713 }) { 19714 const startTime = probe$2.startTime(timescales, data); 19715 this.self.postMessage({ 19716 action: 'probeMp4StartTime', 19717 startTime, 19718 data 19719 }, [data.buffer]); 19720 } 19721 19722 probeMp4Tracks({ 19723 data 19724 }) { 19725 const tracks = probe$2.tracks(data); 19726 this.self.postMessage({ 19727 action: 'probeMp4Tracks', 19728 tracks, 19729 data 19730 }, [data.buffer]); 19731 } 19732 /** 19733 * Probes an mp4 segment for EMSG boxes containing ID3 data. 19734 * https://aomediacodec.github.io/id3-emsg/ 19735 * 19736 * @param {Uint8Array} data segment data 19737 * @param {number} offset segment start time 19738 * @return {Object[]} an array of ID3 frames 19739 */ 19740 19741 19742 probeEmsgID3({ 19743 data, 19744 offset 19745 }) { 19746 const id3Frames = probe$2.getEmsgID3(data, offset); 19747 this.self.postMessage({ 19748 action: 'probeEmsgID3', 19749 id3Frames, 19750 emsgData: data 19751 }, [data.buffer]); 19752 } 19753 /** 19754 * Probe an mpeg2-ts segment to determine the start time of the segment in it's 19755 * internal "media time," as well as whether it contains video and/or audio. 19756 * 19757 * @private 19758 * @param {Uint8Array} bytes - segment bytes 19759 * @param {number} baseStartTime 19760 * Relative reference timestamp used when adjusting frame timestamps for rollover. 19761 * This value should be in seconds, as it's converted to a 90khz clock within the 19762 * function body. 19763 * @return {Object} The start time of the current segment in "media time" as well as 19764 * whether it contains video and/or audio 19765 */ 19766 19767 19768 probeTs({ 19769 data, 19770 baseStartTime 19771 }) { 19772 const tsStartTime = typeof baseStartTime === 'number' && !isNaN(baseStartTime) ? baseStartTime * clock$2.ONE_SECOND_IN_TS : void 0; 19773 const timeInfo = tsInspector.inspect(data, tsStartTime); 19774 let result = null; 19775 19776 if (timeInfo) { 19777 result = { 19778 // each type's time info comes back as an array of 2 times, start and end 19779 hasVideo: timeInfo.video && timeInfo.video.length === 2 || false, 19780 hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false 19781 }; 19782 19783 if (result.hasVideo) { 19784 result.videoStart = timeInfo.video[0].ptsTime; 19785 } 19786 19787 if (result.hasAudio) { 19788 result.audioStart = timeInfo.audio[0].ptsTime; 19789 } 19790 } 19791 19792 this.self.postMessage({ 19793 action: 'probeTs', 19794 result, 19795 data 19796 }, [data.buffer]); 19797 } 19798 19799 clearAllMp4Captions() { 19800 if (this.captionParser) { 19801 this.captionParser.clearAllCaptions(); 19802 } 19803 } 19804 19805 clearParsedMp4Captions() { 19806 if (this.captionParser) { 19807 this.captionParser.clearParsedCaptions(); 19808 } 19809 } 19810 /** 19811 * Adds data (a ts segment) to the start of the transmuxer pipeline for 19812 * processing. 19813 * 19814 * @param {ArrayBuffer} data data to push into the muxer 19815 */ 19816 19817 19818 push(data) { 19819 // Cast array buffer to correct type for transmuxer 19820 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 19821 this.transmuxer.push(segment); 19822 } 19823 /** 19824 * Recreate the transmuxer so that the next segment added via `push` 19825 * start with a fresh transmuxer. 19826 */ 19827 19828 19829 reset() { 19830 this.transmuxer.reset(); 19831 } 19832 /** 19833 * Set the value that will be used as the `baseMediaDecodeTime` time for the 19834 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 19835 * set relative to the first based on the PTS values. 19836 * 19837 * @param {Object} data used to set the timestamp offset in the muxer 19838 */ 19839 19840 19841 setTimestampOffset(data) { 19842 const timestampOffset = data.timestampOffset || 0; 19843 this.transmuxer.setBaseMediaDecodeTime(Math.round(clock$2.secondsToVideoTs(timestampOffset))); 19844 } 19845 19846 setAudioAppendStart(data) { 19847 this.transmuxer.setAudioAppendStart(Math.ceil(clock$2.secondsToVideoTs(data.appendStart))); 19848 } 19849 19850 setRemux(data) { 19851 this.transmuxer.setRemux(data.remux); 19852 } 19853 /** 19854 * Forces the pipeline to finish processing the last segment and emit it's 19855 * results. 19856 * 19857 * @param {Object} data event data, not really used 19858 */ 19859 19860 19861 flush(data) { 19862 this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed 19863 19864 self.postMessage({ 19865 action: 'done', 19866 type: 'transmuxed' 19867 }); 19868 } 19869 19870 endTimeline() { 19871 this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their 19872 // timelines 19873 19874 self.postMessage({ 19875 action: 'endedtimeline', 19876 type: 'transmuxed' 19877 }); 19878 } 19879 19880 alignGopsWith(data) { 19881 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 19882 } 19883 19884 } 19885 /** 19886 * Our web worker interface so that things can talk to mux.js 19887 * that will be running in a web worker. the scope is passed to this by 19888 * webworkify. 19889 * 19890 * @param {Object} self the scope for the web worker 19891 */ 19892 19893 19894 self.onmessage = function (event) { 19895 if (event.data.action === 'init' && event.data.options) { 19896 this.messageHandlers = new MessageHandlers(self, event.data.options); 19897 return; 19898 } 19899 19900 if (!this.messageHandlers) { 19901 this.messageHandlers = new MessageHandlers(self); 19902 } 19903 19904 if (event.data && event.data.action && event.data.action !== 'init') { 19905 if (this.messageHandlers[event.data.action]) { 19906 this.messageHandlers[event.data.action](event.data); 19907 } 19908 } 19909 }; 19910 })); 19911 var TransmuxWorker = factory(workerCode$1); 19912 /* rollup-plugin-worker-factory end for worker!/
vendor: 15,204 bytes, lines 19912-20446
19912home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */ 19913 19914 const handleData_ = (event, transmuxedData, callback) => { 19915 const { 19916 type, 19917 initSegment, 19918 captions, 19919 captionStreams, 19920 metadata, 19921 videoFrameDtsTime, 19922 videoFramePtsTime 19923 } = event.data.segment; 19924 transmuxedData.buffer.push({ 19925 captions, 19926 captionStreams, 19927 metadata 19928 }); 19929 const boxes = event.data.segment.boxes || { 19930 data: event.data.segment.data 19931 }; 19932 const result = { 19933 type, 19934 // cast ArrayBuffer to TypedArray 19935 data: new Uint8Array(boxes.data, boxes.data.byteOffset, boxes.data.byteLength), 19936 initSegment: new Uint8Array(initSegment.data, initSegment.byteOffset, initSegment.byteLength) 19937 }; 19938 19939 if (typeof videoFrameDtsTime !== 'undefined') { 19940 result.videoFrameDtsTime = videoFrameDtsTime; 19941 } 19942 19943 if (typeof videoFramePtsTime !== 'undefined') { 19944 result.videoFramePtsTime = videoFramePtsTime; 19945 } 19946 19947 callback(result); 19948 }; 19949 const handleDone_ = ({ 19950 transmuxedData, 19951 callback 19952 }) => { 19953 // Previously we only returned data on data events, 19954 // not on done events. Clear out the buffer to keep that consistent. 19955 transmuxedData.buffer = []; // all buffers should have been flushed from the muxer, so start processing anything we 19956 // have received 19957 19958 callback(transmuxedData); 19959 }; 19960 const handleGopInfo_ = (event, transmuxedData) => { 19961 transmuxedData.gopInfo = event.data.gopInfo; 19962 }; 19963 const processTransmux = options => { 19964 const { 19965 transmuxer, 19966 bytes, 19967 audioAppendStart, 19968 gopsToAlignWith, 19969 remux, 19970 onData, 19971 onTrackInfo, 19972 onAudioTimingInfo, 19973 onVideoTimingInfo, 19974 onVideoSegmentTimingInfo, 19975 onAudioSegmentTimingInfo, 19976 onId3, 19977 onCaptions, 19978 onDone, 19979 onEndedTimeline, 19980 onTransmuxerLog, 19981 isEndOfTimeline 19982 } = options; 19983 const transmuxedData = { 19984 buffer: [] 19985 }; 19986 let waitForEndedTimelineEvent = isEndOfTimeline; 19987 19988 const handleMessage = event => { 19989 if (transmuxer.currentTransmux !== options) { 19990 // disposed 19991 return; 19992 } 19993 19994 if (event.data.action === 'data') { 19995 handleData_(event, transmuxedData, onData); 19996 } 19997 19998 if (event.data.action === 'trackinfo') { 19999 onTrackInfo(event.data.trackInfo); 20000 } 20001 20002 if (event.data.action === 'gopInfo') { 20003 handleGopInfo_(event, transmuxedData); 20004 } 20005 20006 if (event.data.action === 'audioTimingInfo') { 20007 onAudioTimingInfo(event.data.audioTimingInfo); 20008 } 20009 20010 if (event.data.action === 'videoTimingInfo') { 20011 onVideoTimingInfo(event.data.videoTimingInfo); 20012 } 20013 20014 if (event.data.action === 'videoSegmentTimingInfo') { 20015 onVideoSegmentTimingInfo(event.data.videoSegmentTimingInfo); 20016 } 20017 20018 if (event.data.action === 'audioSegmentTimingInfo') { 20019 onAudioSegmentTimingInfo(event.data.audioSegmentTimingInfo); 20020 } 20021 20022 if (event.data.action === 'id3Frame') { 20023 onId3([event.data.id3Frame], event.data.id3Frame.dispatchType); 20024 } 20025 20026 if (event.data.action === 'caption') { 20027 onCaptions(event.data.caption); 20028 } 20029 20030 if (event.data.action === 'endedtimeline') { 20031 waitForEndedTimelineEvent = false; 20032 onEndedTimeline(); 20033 } 20034 20035 if (event.data.action === 'log') { 20036 onTransmuxerLog(event.data.log); 20037 } // wait for the transmuxed event since we may have audio and video 20038 20039 20040 if (event.data.type !== 'transmuxed') { 20041 return; 20042 } // If the "endedtimeline" event has not yet fired, and this segment represents the end 20043 // of a timeline, that means there may still be data events before the segment 20044 // processing can be considerred complete. In that case, the final event should be 20045 // an "endedtimeline" event with the type "transmuxed." 20046 20047 20048 if (waitForEndedTimelineEvent) { 20049 return; 20050 } 20051 20052 transmuxer.onmessage = null; 20053 handleDone_({ 20054 transmuxedData, 20055 callback: onDone 20056 }); 20057 /* eslint-disable no-use-before-define */ 20058 20059 dequeue(transmuxer); 20060 /* eslint-enable */ 20061 }; 20062 20063 transmuxer.onmessage = handleMessage; 20064 20065 if (audioAppendStart) { 20066 transmuxer.postMessage({ 20067 action: 'setAudioAppendStart', 20068 appendStart: audioAppendStart 20069 }); 20070 } // allow empty arrays to be passed to clear out GOPs 20071 20072 20073 if (Array.isArray(gopsToAlignWith)) { 20074 transmuxer.postMessage({ 20075 action: 'alignGopsWith', 20076 gopsToAlignWith 20077 }); 20078 } 20079 20080 if (typeof remux !== 'undefined') { 20081 transmuxer.postMessage({ 20082 action: 'setRemux', 20083 remux 20084 }); 20085 } 20086 20087 if (bytes.byteLength) { 20088 const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; 20089 const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; 20090 transmuxer.postMessage({ 20091 action: 'push', 20092 // Send the typed-array of data as an ArrayBuffer so that 20093 // it can be sent as a "Transferable" and avoid the costly 20094 // memory copy 20095 data: buffer, 20096 // To recreate the original typed-array, we need information 20097 // about what portion of the ArrayBuffer it was a view into 20098 byteOffset, 20099 byteLength: bytes.byteLength 20100 }, [buffer]); 20101 } 20102 20103 if (isEndOfTimeline) { 20104 transmuxer.postMessage({ 20105 action: 'endTimeline' 20106 }); 20107 } // even if we didn't push any bytes, we have to make sure we flush in case we reached 20108 // the end of the segment 20109 20110 20111 transmuxer.postMessage({ 20112 action: 'flush' 20113 }); 20114 }; 20115 const dequeue = transmuxer => { 20116 transmuxer.currentTransmux = null; 20117 20118 if (transmuxer.transmuxQueue.length) { 20119 transmuxer.currentTransmux = transmuxer.transmuxQueue.shift(); 20120 20121 if (typeof transmuxer.currentTransmux === 'function') { 20122 transmuxer.currentTransmux(); 20123 } else { 20124 processTransmux(transmuxer.currentTransmux); 20125 } 20126 } 20127 }; 20128 const processAction = (transmuxer, action) => { 20129 transmuxer.postMessage({ 20130 action 20131 }); 20132 dequeue(transmuxer); 20133 }; 20134 const enqueueAction = (action, transmuxer) => { 20135 if (!transmuxer.currentTransmux) { 20136 transmuxer.currentTransmux = action; 20137 processAction(transmuxer, action); 20138 return; 20139 } 20140 20141 transmuxer.transmuxQueue.push(processAction.bind(null, transmuxer, action)); 20142 }; 20143 const reset = transmuxer => { 20144 enqueueAction('reset', transmuxer); 20145 }; 20146 const endTimeline = transmuxer => { 20147 enqueueAction('endTimeline', transmuxer); 20148 }; 20149 const transmux = options => { 20150 if (!options.transmuxer.currentTransmux) { 20151 options.transmuxer.currentTransmux = options; 20152 processTransmux(options); 20153 return; 20154 } 20155 20156 options.transmuxer.transmuxQueue.push(options); 20157 }; 20158 const createTransmuxer = options => { 20159 const transmuxer = new TransmuxWorker(); 20160 transmuxer.currentTransmux = null; 20161 transmuxer.transmuxQueue = []; 20162 const term = transmuxer.terminate; 20163 20164 transmuxer.terminate = () => { 20165 transmuxer.currentTransmux = null; 20166 transmuxer.transmuxQueue.length = 0; 20167 return term.call(transmuxer); 20168 }; 20169 20170 transmuxer.postMessage({ 20171 action: 'init', 20172 options 20173 }); 20174 return transmuxer; 20175 }; 20176 var segmentTransmuxer = { 20177 reset, 20178 endTimeline, 20179 transmux, 20180 createTransmuxer 20181 }; 20182 20183 const workerCallback = function (options) { 20184 const transmuxer = options.transmuxer; 20185 const endAction = options.endAction || options.action; 20186 const callback = options.callback; 20187 20188 const message = _extends({}, options, { 20189 endAction: null, 20190 transmuxer: null, 20191 callback: null 20192 }); 20193 20194 const listenForEndEvent = event => { 20195 if (event.data.action !== endAction) { 20196 return; 20197 } 20198 20199 transmuxer.removeEventListener('message', listenForEndEvent); // transfer ownership of bytes back to us. 20200 20201 if (event.data.data) { 20202 event.data.data = new Uint8Array(event.data.data, options.byteOffset || 0, options.byteLength || event.data.data.byteLength); 20203 20204 if (options.data) { 20205 options.data = event.data.data; 20206 } 20207 } 20208 20209 callback(event.data); 20210 }; 20211 20212 transmuxer.addEventListener('message', listenForEndEvent); 20213 20214 if (options.data) { 20215 const isArrayBuffer = options.data instanceof ArrayBuffer; 20216 message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset; 20217 message.byteLength = options.data.byteLength; 20218 const transfers = [isArrayBuffer ? options.data : options.data.buffer]; 20219 transmuxer.postMessage(message, transfers); 20220 } else { 20221 transmuxer.postMessage(message); 20222 } 20223 }; 20224 20225 const REQUEST_ERRORS = { 20226 FAILURE: 2, 20227 TIMEOUT: -101, 20228 ABORTED: -102 20229 }; 20230 /** 20231 * Abort all requests 20232 * 20233 * @param {Object} activeXhrs - an object that tracks all XHR requests 20234 */ 20235 20236 const abortAll = activeXhrs => { 20237 activeXhrs.forEach(xhr => { 20238 xhr.abort(); 20239 }); 20240 }; 20241 /** 20242 * Gather important bandwidth stats once a request has completed 20243 * 20244 * @param {Object} request - the XHR request from which to gather stats 20245 */ 20246 20247 20248 const getRequestStats = request => { 20249 return { 20250 bandwidth: request.bandwidth, 20251 bytesReceived: request.bytesReceived || 0, 20252 roundTripTime: request.roundTripTime || 0 20253 }; 20254 }; 20255 /** 20256 * If possible gather bandwidth stats as a request is in 20257 * progress 20258 * 20259 * @param {Event} progressEvent - an event object from an XHR's progress event 20260 */ 20261 20262 20263 const getProgressStats = progressEvent => { 20264 const request = progressEvent.target; 20265 const roundTripTime = Date.now() - request.requestTime; 20266 const stats = { 20267 bandwidth: Infinity, 20268 bytesReceived: 0, 20269 roundTripTime: roundTripTime || 0 20270 }; 20271 stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok 20272 // because we should only use bandwidth stats on progress to determine when 20273 // abort a request early due to insufficient bandwidth 20274 20275 stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); 20276 return stats; 20277 }; 20278 /** 20279 * Handle all error conditions in one place and return an object 20280 * with all the information 20281 * 20282 * @param {Error|null} error - if non-null signals an error occured with the XHR 20283 * @param {Object} request - the XHR request that possibly generated the error 20284 */ 20285 20286 20287 const handleErrors = (error, request) => { 20288 if (request.timedout) { 20289 return { 20290 status: request.status, 20291 message: 'HLS request timed-out at URL: ' + request.uri, 20292 code: REQUEST_ERRORS.TIMEOUT, 20293 xhr: request 20294 }; 20295 } 20296 20297 if (request.aborted) { 20298 return { 20299 status: request.status, 20300 message: 'HLS request aborted at URL: ' + request.uri, 20301 code: REQUEST_ERRORS.ABORTED, 20302 xhr: request 20303 }; 20304 } 20305 20306 if (error) { 20307 return { 20308 status: request.status, 20309 message: 'HLS request errored at URL: ' + request.uri, 20310 code: REQUEST_ERRORS.FAILURE, 20311 xhr: request 20312 }; 20313 } 20314 20315 if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) { 20316 return { 20317 status: request.status, 20318 message: 'Empty HLS response at URL: ' + request.uri, 20319 code: REQUEST_ERRORS.FAILURE, 20320 xhr: request 20321 }; 20322 } 20323 20324 return null; 20325 }; 20326 /** 20327 * Handle responses for key data and convert the key data to the correct format 20328 * for the decryption step later 20329 * 20330 * @param {Object} segment - a simplified copy of the segmentInfo object 20331 * from SegmentLoader 20332 * @param {Array} objects - objects to add the key bytes to. 20333 * @param {Function} finishProcessingFn - a callback to execute to continue processing 20334 * this request 20335 */ 20336 20337 20338 const handleKeyResponse = (segment, objects, finishProcessingFn) => (error, request) => { 20339 const response = request.response; 20340 const errorObj = handleErrors(error, request); 20341 20342 if (errorObj) { 20343 return finishProcessingFn(errorObj, segment); 20344 } 20345 20346 if (response.byteLength !== 16) { 20347 return finishProcessingFn({ 20348 status: request.status, 20349 message: 'Invalid HLS key at URL: ' + request.uri, 20350 code: REQUEST_ERRORS.FAILURE, 20351 xhr: request 20352 }, segment); 20353 } 20354 20355 const view = new DataView(response); 20356 const bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); 20357 20358 for (let i = 0; i < objects.length; i++) { 20359 objects[i].bytes = bytes; 20360 } 20361 20362 return finishProcessingFn(null, segment); 20363 }; 20364 20365 const parseInitSegment = (segment, callback) => { 20366 const type = detectContainerForBytes(segment.map.bytes); // TODO: We should also handle ts init segments here, but we 20367 // only know how to parse mp4 init segments at the moment 20368 20369 if (type !== 'mp4') { 20370 const uri = segment.map.resolvedUri || segment.map.uri; 20371 return callback({ 20372 internal: true, 20373 message: `Found unsupported ${type || 'unknown'} container for initialization segment at URL: ${uri}`, 20374 code: REQUEST_ERRORS.FAILURE 20375 }); 20376 } 20377 20378 workerCallback({ 20379 action: 'probeMp4Tracks', 20380 data: segment.map.bytes, 20381 transmuxer: segment.transmuxer, 20382 callback: ({ 20383 tracks, 20384 data 20385 }) => { 20386 // transfer bytes back to us 20387 segment.map.bytes = data; 20388 tracks.forEach(function (track) { 20389 segment.map.tracks = segment.map.tracks || {}; // only support one track of each type for now 20390 20391 if (segment.map.tracks[track.type]) { 20392 return; 20393 } 20394 20395 segment.map.tracks[track.type] = track; 20396 20397 if (typeof track.id === 'number' && track.timescale) { 20398 segment.map.timescales = segment.map.timescales || {}; 20399 segment.map.timescales[track.id] = track.timescale; 20400 } 20401 }); 20402 return callback(null); 20403 } 20404 }); 20405 }; 20406 /** 20407 * Handle init-segment responses 20408 * 20409 * @param {Object} segment - a simplified copy of the segmentInfo object 20410 * from SegmentLoader 20411 * @param {Function} finishProcessingFn - a callback to execute to continue processing 20412 * this request 20413 */ 20414 20415 20416 const handleInitSegmentResponse = ({ 20417 segment, 20418 finishProcessingFn 20419 }) => (error, request) => { 20420 const errorObj = handleErrors(error, request); 20421 20422 if (errorObj) { 20423 return finishProcessingFn(errorObj, segment); 20424 } 20425 20426 const bytes = new Uint8Array(request.response); // init segment is encypted, we will have to wait 20427 // until the key request is done to decrypt. 20428 20429 if (segment.map.key) { 20430 segment.map.encryptedBytes = bytes; 20431 return finishProcessingFn(null, segment); 20432 } 20433 20434 segment.map.bytes = bytes; 20435 parseInitSegment(segment, function (parseError) { 20436 if (parseError) { 20437 parseError.xhr = request; 20438 parseError.status = request.status; 20439 return finishProcessingFn(parseError, segment); 20440 } 20441 20442 finishProcessingFn(null, segment); 20443 }); 20444 }; 20445 /** 20446 * Response handler for segment-requests being sure to set the correct
20447 * property depending on whether the segment is encryped or not 20448 * Also records and keeps track of stats that are used for ABR purposes 20449 * 20450 * @param {Object} segment - a simplified copy of the segmentInfo object 20451 * from SegmentLoader 20452 * @param {Function} finishProcessingFn - a callback to execute to continue processing 20453 * this request 20454 */ 20455 20456 20457 const handleSegmentResponse = ({ 20458 segment, 20459 finishProcessingFn, 20460 responseType 20461 }) => (error, request) => { 20462 const errorObj = handleErrors(error, request); 20463 20464 if (errorObj) { 20465 return finishProcessingFn(errorObj, segment); 20466 } 20467 20468 const newBytes = // although responseText "should" exist, this guard serves to prevent an error being 20469 // thrown for two primary cases: 20470 // 1. the mime type override stops working, or is not implemented for a specific 20471 // browser 20472 // 2. when using mock XHR libraries like sinon that do not allow the override behavior 20473 responseType === 'arraybuffer' || !request.responseText ? request.response : stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0)); 20474 segment.stats = getRequestStats(request); 20475 20476 if (segment.key) { 20477 segment.encryptedBytes = new Uint8Array(newBytes); 20478 } else { 20479 segment.bytes = new Uint8Array(newBytes); 20480 } 20481 20482 return finishProcessingFn(null, segment); 20483 }; 20484 20485 const transmuxAndNotify = ({ 20486 segment, 20487 bytes, 20488 trackInfoFn, 20489 timingInfoFn, 20490 videoSegmentTimingInfoFn, 20491 audioSegmentTimingInfoFn, 20492 id3Fn, 20493 captionsFn, 20494 isEndOfTimeline, 20495 endedTimelineFn, 20496 dataFn, 20497 doneFn, 20498 onTransmuxerLog 20499 }) => { 20500 const fmp4Tracks = segment.map && segment.map.tracks || {}; 20501 const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); // Keep references to each function so we can null them out after we're done with them. 20502 // One reason for this is that in the case of full segments, we want to trust start 20503 // times from the probe, rather than the transmuxer. 20504 20505 let audioStartFn = timingInfoFn.bind(null, segment, 'audio', 'start'); 20506 const audioEndFn = timingInfoFn.bind(null, segment, 'audio', 'end'); 20507 let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start'); 20508 const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end'); 20509 20510 const finish = () => transmux({ 20511 bytes, 20512 transmuxer: segment.transmuxer, 20513 audioAppendStart: segment.audioAppendStart, 20514 gopsToAlignWith: segment.gopsToAlignWith, 20515 remux: isMuxed, 20516 onData: result => { 20517 result.type = result.type === 'combined' ? 'video' : result.type; 20518 dataFn(segment, result); 20519 }, 20520 onTrackInfo: trackInfo => { 20521 if (trackInfoFn) { 20522 if (isMuxed) { 20523 trackInfo.isMuxed = true; 20524 } 20525 20526 trackInfoFn(segment, trackInfo); 20527 } 20528 }, 20529 onAudioTimingInfo: audioTimingInfo => { 20530 // we only want the first start value we encounter 20531 if (audioStartFn && typeof audioTimingInfo.start !== 'undefined') { 20532 audioStartFn(audioTimingInfo.start); 20533 audioStartFn = null; 20534 } // we want to continually update the end time 20535 20536 20537 if (audioEndFn && typeof audioTimingInfo.end !== 'undefined') { 20538 audioEndFn(audioTimingInfo.end); 20539 } 20540 }, 20541 onVideoTimingInfo: videoTimingInfo => { 20542 // we only want the first start value we encounter 20543 if (videoStartFn && typeof videoTimingInfo.start !== 'undefined') { 20544 videoStartFn(videoTimingInfo.start); 20545 videoStartFn = null; 20546 } // we want to continually update the end time 20547 20548 20549 if (videoEndFn && typeof videoTimingInfo.end !== 'undefined') { 20550 videoEndFn(videoTimingInfo.end); 20551 } 20552 }, 20553 onVideoSegmentTimingInfo: videoSegmentTimingInfo => { 20554 videoSegmentTimingInfoFn(videoSegmentTimingInfo); 20555 }, 20556 onAudioSegmentTimingInfo: audioSegmentTimingInfo => { 20557 audioSegmentTimingInfoFn(audioSegmentTimingInfo); 20558 }, 20559 onId3: (id3Frames, dispatchType) => { 20560 id3Fn(segment, id3Frames, dispatchType); 20561 }, 20562 onCaptions: captions => { 20563 captionsFn(segment, [captions]); 20564 }, 20565 isEndOfTimeline, 20566 onEndedTimeline: () => { 20567 endedTimelineFn(); 20568 }, 20569 onTransmuxerLog, 20570 onDone: result => { 20571 if (!doneFn) { 20572 return; 20573 } 20574 20575 result.type = result.type === 'combined' ? 'video' : result.type; 20576 doneFn(null, segment, result); 20577 } 20578 }); // In the transmuxer, we don't yet have the ability to extract a "proper" start time. 20579 // Meaning cached frame data may corrupt our notion of where this segment 20580 // really starts. To get around this, probe for the info needed. 20581 20582 20583 workerCallback({ 20584 action: 'probeTs', 20585 transmuxer: segment.transmuxer, 20586 data: bytes, 20587 baseStartTime: segment.baseStartTime, 20588 callback: data => { 20589 segment.bytes = bytes = data.data; 20590 const probeResult = data.result; 20591 20592 if (probeResult) { 20593 trackInfoFn(segment, { 20594 hasAudio: probeResult.hasAudio, 20595 hasVideo: probeResult.hasVideo, 20596 isMuxed 20597 }); 20598 trackInfoFn = null; 20599 } 20600 20601 finish(); 20602 } 20603 }); 20604 }; 20605 20606 const handleSegmentBytes = ({ 20607 segment, 20608 bytes, 20609 trackInfoFn, 20610 timingInfoFn, 20611 videoSegmentTimingInfoFn, 20612 audioSegmentTimingInfoFn, 20613 id3Fn, 20614 captionsFn, 20615 isEndOfTimeline, 20616 endedTimelineFn, 20617 dataFn, 20618 doneFn, 20619 onTransmuxerLog 20620 }) => { 20621 let bytesAsUint8Array = new Uint8Array(bytes); // TODO: 20622 // We should have a handler that fetches the number of bytes required 20623 // to check if something is fmp4. This will allow us to save bandwidth 20624 // because we can only exclude a playlist and abort requests 20625 // by codec after trackinfo triggers. 20626 20627 if (isLikelyFmp4MediaSegment(bytesAsUint8Array)) { 20628 segment.isFmp4 = true; 20629 const { 20630 tracks 20631 } = segment.map; 20632 const trackInfo = { 20633 isFmp4: true, 20634 hasVideo: !!tracks.video, 20635 hasAudio: !!tracks.audio 20636 }; // if we have a audio track, with a codec that is not set to 20637 // encrypted audio 20638 20639 if (tracks.audio && tracks.audio.codec && tracks.audio.codec !== 'enca') { 20640 trackInfo.audioCodec = tracks.audio.codec; 20641 } // if we have a video track, with a codec that is not set to 20642 // encrypted video 20643 20644 20645 if (tracks.video && tracks.video.codec && tracks.video.codec !== 'encv') { 20646 trackInfo.videoCodec = tracks.video.codec; 20647 } 20648 20649 if (tracks.video && tracks.audio) { 20650 trackInfo.isMuxed = true; 20651 } // since we don't support appending fmp4 data on progress, we know
vendor: 18,596 bytes, lines 20651-21185
20651we have the full 20652 // segment here 20653 20654 20655 trackInfoFn(segment, trackInfo); // The probe doesn't provide the segment end time, so only callback with the start 20656 // time. The end time can be roughly calculated by the receiver using the duration. 20657 // 20658 // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as 20659 // that is the true start of the segment (where the playback engine should begin 20660 // decoding). 20661 20662 const finishLoading = (captions, id3Frames) => { 20663 // if the track still has audio at this point it is only possible 20664 // for it to be audio only. See `tracks.video && tracks.audio` if statement 20665 // above. 20666 // we make sure to use segment.bytes here as that 20667 dataFn(segment, { 20668 data: bytesAsUint8Array, 20669 type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video' 20670 }); 20671 20672 if (id3Frames && id3Frames.length) { 20673 id3Fn(segment, id3Frames); 20674 } 20675 20676 if (captions && captions.length) { 20677 captionsFn(segment, captions); 20678 } 20679 20680 doneFn(null, segment, {}); 20681 }; 20682 20683 workerCallback({ 20684 action: 'probeMp4StartTime', 20685 timescales: segment.map.timescales, 20686 data: bytesAsUint8Array, 20687 transmuxer: segment.transmuxer, 20688 callback: ({ 20689 data, 20690 startTime 20691 }) => { 20692 // transfer bytes back to us 20693 bytes = data.buffer; 20694 segment.bytes = bytesAsUint8Array = data; 20695 20696 if (trackInfo.hasAudio && !trackInfo.isMuxed) { 20697 timingInfoFn(segment, 'audio', 'start', startTime); 20698 } 20699 20700 if (trackInfo.hasVideo) { 20701 timingInfoFn(segment, 'video', 'start', startTime); 20702 } 20703 20704 workerCallback({ 20705 action: 'probeEmsgID3', 20706 data: bytesAsUint8Array, 20707 transmuxer: segment.transmuxer, 20708 offset: startTime, 20709 callback: ({ 20710 emsgData, 20711 id3Frames 20712 }) => { 20713 // transfer bytes back to us 20714 bytes = emsgData.buffer; 20715 segment.bytes = bytesAsUint8Array = emsgData; // Run through the CaptionParser in case there are captions. 20716 // Initialize CaptionParser if it hasn't been yet 20717 20718 if (!tracks.video || !emsgData.byteLength || !segment.transmuxer) { 20719 finishLoading(undefined, id3Frames); 20720 return; 20721 } 20722 20723 workerCallback({ 20724 action: 'pushMp4Captions', 20725 endAction: 'mp4Captions', 20726 transmuxer: segment.transmuxer, 20727 data: bytesAsUint8Array, 20728 timescales: segment.map.timescales, 20729 trackIds: [tracks.video.id], 20730 callback: message => { 20731 // transfer bytes back to us 20732 bytes = message.data.buffer; 20733 segment.bytes = bytesAsUint8Array = message.data; 20734 message.logs.forEach(function (log) { 20735 onTransmuxerLog(merge$1(log, { 20736 stream: 'mp4CaptionParser' 20737 })); 20738 }); 20739 finishLoading(message.captions, id3Frames); 20740 } 20741 }); 20742 } 20743 }); 20744 } 20745 }); 20746 return; 20747 } // VTT or other segments that don't need processing 20748 20749 20750 if (!segment.transmuxer) { 20751 doneFn(null, segment, {}); 20752 return; 20753 } 20754 20755 if (typeof segment.container === 'undefined') { 20756 segment.container = detectContainerForBytes(bytesAsUint8Array); 20757 } 20758 20759 if (segment.container !== 'ts' && segment.container !== 'aac') { 20760 trackInfoFn(segment, { 20761 hasAudio: false, 20762 hasVideo: false 20763 }); 20764 doneFn(null, segment, {}); 20765 return; 20766 } // ts or aac 20767 20768 20769 transmuxAndNotify({ 20770 segment, 20771 bytes, 20772 trackInfoFn, 20773 timingInfoFn, 20774 videoSegmentTimingInfoFn, 20775 audioSegmentTimingInfoFn, 20776 id3Fn, 20777 captionsFn, 20778 isEndOfTimeline, 20779 endedTimelineFn, 20780 dataFn, 20781 doneFn, 20782 onTransmuxerLog 20783 }); 20784 }; 20785 20786 const decrypt = function ({ 20787 id, 20788 key, 20789 encryptedBytes, 20790 decryptionWorker 20791 }, callback) { 20792 const decryptionHandler = event => { 20793 if (event.data.source === id) { 20794 decryptionWorker.removeEventListener('message', decryptionHandler); 20795 const decrypted = event.data.decrypted; 20796 callback(new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength)); 20797 } 20798 }; 20799 20800 decryptionWorker.addEventListener('message', decryptionHandler); 20801 let keyBytes; 20802 20803 if (key.bytes.slice) { 20804 keyBytes = key.bytes.slice(); 20805 } else { 20806 keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes)); 20807 } // incrementally decrypt the bytes 20808 20809 20810 decryptionWorker.postMessage(createTransferableMessage({ 20811 source: id, 20812 encrypted: encryptedBytes, 20813 key: keyBytes, 20814 iv: key.iv 20815 }), [encryptedBytes.buffer, keyBytes.buffer]); 20816 }; 20817 /** 20818 * Decrypt the segment via the decryption web worker 20819 * 20820 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 20821 * routines 20822 * @param {Object} segment - a simplified copy of the segmentInfo object 20823 * from SegmentLoader 20824 * @param {Function} trackInfoFn - a callback that receives track info 20825 * @param {Function} timingInfoFn - a callback that receives timing info 20826 * @param {Function} videoSegmentTimingInfoFn 20827 * a callback that receives video timing info based on media times and 20828 * any adjustments made by the transmuxer 20829 * @param {Function} audioSegmentTimingInfoFn 20830 * a callback that receives audio timing info based on media times and 20831 * any adjustments made by the transmuxer 20832 * @param {boolean} isEndOfTimeline 20833 * true if this segment represents the last segment in a timeline 20834 * @param {Function} endedTimelineFn 20835 * a callback made when a timeline is ended, will only be called if 20836 * isEndOfTimeline is true 20837 * @param {Function} dataFn - a callback that is executed when segment bytes are available 20838 * and ready to use 20839 * @param {Function} doneFn - a callback that is executed after decryption has completed 20840 */ 20841 20842 20843 const decryptSegment = ({ 20844 decryptionWorker, 20845 segment, 20846 trackInfoFn, 20847 timingInfoFn, 20848 videoSegmentTimingInfoFn, 20849 audioSegmentTimingInfoFn, 20850 id3Fn, 20851 captionsFn, 20852 isEndOfTimeline, 20853 endedTimelineFn, 20854 dataFn, 20855 doneFn, 20856 onTransmuxerLog 20857 }) => { 20858 decrypt({ 20859 id: segment.requestId, 20860 key: segment.key, 20861 encryptedBytes: segment.encryptedBytes, 20862 decryptionWorker 20863 }, decryptedBytes => { 20864 segment.bytes = decryptedBytes; 20865 handleSegmentBytes({ 20866 segment, 20867 bytes: segment.bytes, 20868 trackInfoFn, 20869 timingInfoFn, 20870 videoSegmentTimingInfoFn, 20871 audioSegmentTimingInfoFn, 20872 id3Fn, 20873 captionsFn, 20874 isEndOfTimeline, 20875 endedTimelineFn, 20876 dataFn, 20877 doneFn, 20878 onTransmuxerLog 20879 }); 20880 }); 20881 }; 20882 /** 20883 * This function waits for all XHRs to finish (with either success or failure) 20884 * before continueing processing via it's callback. The function gathers errors 20885 * from each request into a single errors array so that the error status for 20886 * each request can be examined later. 20887 * 20888 * @param {Object} activeXhrs - an object that tracks all XHR requests 20889 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 20890 * routines 20891 * @param {Function} trackInfoFn - a callback that receives track info 20892 * @param {Function} timingInfoFn - a callback that receives timing info 20893 * @param {Function} videoSegmentTimingInfoFn 20894 * a callback that receives video timing info based on media times and 20895 * any adjustments made by the transmuxer 20896 * @param {Function} audioSegmentTimingInfoFn 20897 * a callback that receives audio timing info based on media times and 20898 * any adjustments made by the transmuxer 20899 * @param {Function} id3Fn - a callback that receives ID3 metadata 20900 * @param {Function} captionsFn - a callback that receives captions 20901 * @param {boolean} isEndOfTimeline 20902 * true if this segment represents the last segment in a timeline 20903 * @param {Function} endedTimelineFn 20904 * a callback made when a timeline is ended, will only be called if 20905 * isEndOfTimeline is true 20906 * @param {Function} dataFn - a callback that is executed when segment bytes are available 20907 * and ready to use 20908 * @param {Function} doneFn - a callback that is executed after all resources have been 20909 * downloaded and any decryption completed 20910 */ 20911 20912 20913 const waitForCompletion = ({ 20914 activeXhrs, 20915 decryptionWorker, 20916 trackInfoFn, 20917 timingInfoFn, 20918 videoSegmentTimingInfoFn, 20919 audioSegmentTimingInfoFn, 20920 id3Fn, 20921 captionsFn, 20922 isEndOfTimeline, 20923 endedTimelineFn, 20924 dataFn, 20925 doneFn, 20926 onTransmuxerLog 20927 }) => { 20928 let count = 0; 20929 let didError = false; 20930 return (error, segment) => { 20931 if (didError) { 20932 return; 20933 } 20934 20935 if (error) { 20936 didError = true; // If there are errors, we have to abort any outstanding requests 20937 20938 abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we 20939 // handle the aborted events from those requests, there are some cases where we may 20940 // never get an aborted event. For instance, if the network connection is lost and 20941 // there were two requests, the first may have triggered an error immediately, while 20942 // the second request remains unsent. In that case, the aborted algorithm will not 20943 // trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method 20944 // 20945 // We also can't rely on the ready state of the XHR, since the request that 20946 // triggered the connection error may also show as a ready state of 0 (unsent). 20947 // Therefore, we have to finish this group of requests immediately after the first 20948 // seen error. 20949 20950 return doneFn(error, segment); 20951 } 20952 20953 count += 1; 20954 20955 if (count === activeXhrs.length) { 20956 const segmentFinish = function () { 20957 if (segment.encryptedBytes) { 20958 return decryptSegment({ 20959 decryptionWorker, 20960 segment, 20961 trackInfoFn, 20962 timingInfoFn, 20963 videoSegmentTimingInfoFn, 20964 audioSegmentTimingInfoFn, 20965 id3Fn, 20966 captionsFn, 20967 isEndOfTimeline, 20968 endedTimelineFn, 20969 dataFn, 20970 doneFn, 20971 onTransmuxerLog 20972 }); 20973 } // Otherwise, everything is ready just continue 20974 20975 20976 handleSegmentBytes({ 20977 segment, 20978 bytes: segment.bytes, 20979 trackInfoFn, 20980 timingInfoFn, 20981 videoSegmentTimingInfoFn, 20982 audioSegmentTimingInfoFn, 20983 id3Fn, 20984 captionsFn, 20985 isEndOfTimeline, 20986 endedTimelineFn, 20987 dataFn, 20988 doneFn, 20989 onTransmuxerLog 20990 }); 20991 }; // Keep track of when *all* of the requests have completed 20992 20993 20994 segment.endOfAllRequests = Date.now(); 20995 20996 if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) { 20997 return decrypt({ 20998 decryptionWorker, 20999 // add -init to the "id" to differentiate between segment 21000 // and init segment decryption, just in case they happen 21001 // at the same time at some point in the future. 21002 id: segment.requestId + '-init', 21003 encryptedBytes: segment.map.encryptedBytes, 21004 key: segment.map.key 21005 }, decryptedBytes => { 21006 segment.map.bytes = decryptedBytes; 21007 parseInitSegment(segment, parseError => { 21008 if (parseError) { 21009 abortAll(activeXhrs); 21010 return doneFn(parseError, segment); 21011 } 21012 21013 segmentFinish(); 21014 }); 21015 }); 21016 } 21017 21018 segmentFinish(); 21019 } 21020 }; 21021 }; 21022 /** 21023 * Calls the abort callback if any request within the batch was aborted. Will only call 21024 * the callback once per batch of requests, even if multiple were aborted. 21025 * 21026 * @param {Object} loadendState - state to check to see if the abort function was called 21027 * @param {Function} abortFn - callback to call for abort 21028 */ 21029 21030 21031 const handleLoadEnd = ({ 21032 loadendState, 21033 abortFn 21034 }) => event => { 21035 const request = event.target; 21036 21037 if (request.aborted && abortFn && !loadendState.calledAbortFn) { 21038 abortFn(); 21039 loadendState.calledAbortFn = true; 21040 } 21041 }; 21042 /** 21043 * Simple progress event callback handler that gathers some stats before 21044 * executing a provided callback with the `segment` object 21045 * 21046 * @param {Object} segment - a simplified copy of the segmentInfo object 21047 * from SegmentLoader 21048 * @param {Function} progressFn - a callback that is executed each time a progress event 21049 * is received 21050 * @param {Function} trackInfoFn - a callback that receives track info 21051 * @param {Function} timingInfoFn - a callback that receives timing info 21052 * @param {Function} videoSegmentTimingInfoFn 21053 * a callback that receives video timing info based on media times and 21054 * any adjustments made by the transmuxer 21055 * @param {Function} audioSegmentTimingInfoFn 21056 * a callback that receives audio timing info based on media times and 21057 * any adjustments made by the transmuxer 21058 * @param {boolean} isEndOfTimeline 21059 * true if this segment represents the last segment in a timeline 21060 * @param {Function} endedTimelineFn 21061 * a callback made when a timeline is ended, will only be called if 21062 * isEndOfTimeline is true 21063 * @param {Function} dataFn - a callback that is executed when segment bytes are available 21064 * and ready to use 21065 * @param {Event} event - the progress event object from XMLHttpRequest 21066 */ 21067 21068 21069 const handleProgress = ({ 21070 segment, 21071 progressFn, 21072 trackInfoFn, 21073 timingInfoFn, 21074 videoSegmentTimingInfoFn, 21075 audioSegmentTimingInfoFn, 21076 id3Fn, 21077 captionsFn, 21078 isEndOfTimeline, 21079 endedTimelineFn, 21080 dataFn 21081 }) => event => { 21082 const request = event.target; 21083 21084 if (request.aborted) { 21085 return; 21086 } 21087 21088 segment.stats = merge$1(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data 21089 21090 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 21091 segment.stats.firstBytesReceivedAt = Date.now(); 21092 } 21093 21094 return progressFn(event, segment); 21095 }; 21096 /** 21097 * Load all resources and does any processing necessary for a media-segment 21098 * 21099 * Features: 21100 * decrypts the media-segment if it has a key uri and an iv 21101 * aborts *all* requests if *any* one request fails 21102 * 21103 * The segment object, at minimum, has the following format: 21104 * { 21105 * resolvedUri: String, 21106 * [transmuxer]: Object, 21107 * [byterange]: { 21108 * offset: Number, 21109 * length: Number 21110 * }, 21111 * [key]: { 21112 * resolvedUri: String 21113 * [byterange]: { 21114 * offset: Number, 21115 * length: Number 21116 * }, 21117 * iv: { 21118 * bytes: Uint32Array 21119 * } 21120 * }, 21121 * [map]: { 21122 * resolvedUri: String, 21123 * [byterange]: { 21124 * offset: Number, 21125 * length: Number 21126 * }, 21127 * [bytes]: Uint8Array 21128 * } 21129 * } 21130 * ...where [name] denotes optional properties 21131 * 21132 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 21133 * @param {Object} xhrOptions - the base options to provide to all xhr requests 21134 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 21135 * decryption routines 21136 * @param {Object} segment - a simplified copy of the segmentInfo object 21137 * from SegmentLoader 21138 * @param {Function} abortFn - a callback called (only once) if any piece of a request was 21139 * aborted 21140 * @param {Function} progressFn - a callback that receives progress events from the main 21141 * segment's xhr request 21142 * @param {Function} trackInfoFn - a callback that receives track info 21143 * @param {Function} timingInfoFn - a callback that receives timing info 21144 * @param {Function} videoSegmentTimingInfoFn 21145 * a callback that receives video timing info based on media times and 21146 * any adjustments made by the transmuxer 21147 * @param {Function} audioSegmentTimingInfoFn 21148 * a callback that receives audio timing info based on media times and 21149 * any adjustments made by the transmuxer 21150 * @param {Function} id3Fn - a callback that receives ID3 metadata 21151 * @param {Function} captionsFn - a callback that receives captions 21152 * @param {boolean} isEndOfTimeline 21153 * true if this segment represents the last segment in a timeline 21154 * @param {Function} endedTimelineFn 21155 * a callback made when a timeline is ended, will only be called if 21156 * isEndOfTimeline is true 21157 * @param {Function} dataFn - a callback that receives data from the main segment's xhr 21158 * request, transmuxed if needed 21159 * @param {Function} doneFn - a callback that is executed only once all requests have 21160 * succeeded or failed 21161 * @return {Function} a function that, when invoked, immediately aborts all 21162 * outstanding requests 21163 */ 21164 21165 21166 const mediaSegmentRequest = ({ 21167 xhr, 21168 xhrOptions, 21169 decryptionWorker, 21170 segment, 21171 abortFn, 21172 progressFn, 21173 trackInfoFn, 21174 timingInfoFn, 21175 videoSegmentTimingInfoFn, 21176 audioSegmentTimingInfoFn, 21177 id3Fn, 21178 captionsFn, 21179 isEndOfTimeline, 21180 endedTimelineFn, 21181 dataFn, 21182 doneFn, 21183 onTransmuxerLog 21184 }) => { 21185 const activeXhrs = [];
vendor: 4,936 bytes, lines 21186-21327
21186 const finishProcessingFn = waitForCompletion({ 21187 activeXhrs, 21188 decryptionWorker, 21189 trackInfoFn, 21190 timingInfoFn, 21191 videoSegmentTimingInfoFn, 21192 audioSegmentTimingInfoFn, 21193 id3Fn, 21194 captionsFn, 21195 isEndOfTimeline, 21196 endedTimelineFn, 21197 dataFn, 21198 doneFn, 21199 onTransmuxerLog 21200 }); // optionally, request the decryption key 21201 21202 if (segment.key && !segment.key.bytes) { 21203 const objects = [segment.key]; 21204 21205 if (segment.map && !segment.map.bytes && segment.map.key && segment.map.key.resolvedUri === segment.key.resolvedUri) { 21206 objects.push(segment.map.key); 21207 } 21208 21209 const keyRequestOptions = merge$1(xhrOptions, { 21210 uri: segment.key.resolvedUri, 21211 responseType: 'arraybuffer' 21212 }); 21213 const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn); 21214 const keyXhr = xhr(keyRequestOptions, keyRequestCallback); 21215 activeXhrs.push(keyXhr); 21216 } // optionally, request the associated media init segment 21217 21218 21219 if (segment.map && !segment.map.bytes) { 21220 const differentMapKey = segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri); 21221 21222 if (differentMapKey) { 21223 const mapKeyRequestOptions = merge$1(xhrOptions, { 21224 uri: segment.map.key.resolvedUri, 21225 responseType: 'arraybuffer' 21226 }); 21227 const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn); 21228 const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); 21229 activeXhrs.push(mapKeyXhr); 21230 } 21231 21232 const initSegmentOptions = merge$1(xhrOptions, { 21233 uri: segment.map.resolvedUri, 21234 responseType: 'arraybuffer', 21235 headers: segmentXhrHeaders(segment.map) 21236 }); 21237 const initSegmentRequestCallback = handleInitSegmentResponse({ 21238 segment, 21239 finishProcessingFn 21240 }); 21241 const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 21242 activeXhrs.push(initSegmentXhr); 21243 } 21244 21245 const segmentRequestOptions = merge$1(xhrOptions, { 21246 uri: segment.part && segment.part.resolvedUri || segment.resolvedUri, 21247 responseType: 'arraybuffer', 21248 headers: segmentXhrHeaders(segment) 21249 }); 21250 const segmentRequestCallback = handleSegmentResponse({ 21251 segment, 21252 finishProcessingFn, 21253 responseType: segmentRequestOptions.responseType 21254 }); 21255 const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 21256 segmentXhr.addEventListener('progress', handleProgress({ 21257 segment, 21258 progressFn, 21259 trackInfoFn, 21260 timingInfoFn, 21261 videoSegmentTimingInfoFn, 21262 audioSegmentTimingInfoFn, 21263 id3Fn, 21264 captionsFn, 21265 isEndOfTimeline, 21266 endedTimelineFn, 21267 dataFn 21268 })); 21269 activeXhrs.push(segmentXhr); // since all parts of the request must be considered, but should not make callbacks 21270 // multiple times, provide a shared state object 21271 21272 const loadendState = {}; 21273 activeXhrs.forEach(activeXhr => { 21274 activeXhr.addEventListener('loadend', handleLoadEnd({ 21275 loadendState, 21276 abortFn 21277 })); 21278 }); 21279 return () => abortAll(activeXhrs); 21280 }; 21281 21282 /** 21283 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 21284 * codec strings, or translating codec strings into objects that can be examined. 21285 */ 21286 const logFn$1 = logger('CodecUtils'); 21287 /** 21288 * Returns a set of codec strings parsed from the playlist or the default 21289 * codec strings if no codecs were specified in the playlist 21290 * 21291 * @param {Playlist} media the current media playlist 21292 * @return {Object} an object with the video and audio codecs 21293 */ 21294 21295 const getCodecs = function (media) { 21296 // if the codecs were explicitly specified, use them instead of the 21297 // defaults 21298 const mediaAttributes = media.attributes || {}; 21299 21300 if (mediaAttributes.CODECS) { 21301 return parseCodecs(mediaAttributes.CODECS); 21302 } 21303 }; 21304 21305 const isMaat = (main, media) => { 21306 const mediaAttributes = media.attributes || {}; 21307 return main && main.mediaGroups && main.mediaGroups.AUDIO && mediaAttributes.AUDIO && main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 21308 }; 21309 const isMuxed = (main, media) => { 21310 if (!isMaat(main, media)) { 21311 return true; 21312 } 21313 21314 const mediaAttributes = media.attributes || {}; 21315 const audioGroup = main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 21316 21317 for (const groupId in audioGroup) { 21318 // If an audio group has a URI (the case for HLS, as HLS will use external playlists), 21319 // or there are listed playlists (the case for DASH, as the manifest will have already 21320 // provided all of the details necessary to generate the audio playlist, as opposed to 21321 // HLS' externally requested playlists), then the content is demuxed. 21322 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 21323 return true; 21324 } 21325 } 21326 21327 return false;
vendor: 5,350 bytes, lines 21328-21504
21328 }; 21329 const unwrapCodecList = function (codecList) { 21330 const codecs = {}; 21331 codecList.forEach(({ 21332 mediaType, 21333 type, 21334 details 21335 }) => { 21336 codecs[mediaType] = codecs[mediaType] || []; 21337 codecs[mediaType].push(translateLegacyCodec(`${type}${details}`)); 21338 }); 21339 Object.keys(codecs).forEach(function (mediaType) { 21340 if (codecs[mediaType].length > 1) { 21341 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.`); 21342 codecs[mediaType] = null; 21343 return; 21344 } 21345 21346 codecs[mediaType] = codecs[mediaType][0]; 21347 }); 21348 return codecs; 21349 }; 21350 const codecCount = function (codecObj) { 21351 let count = 0; 21352 21353 if (codecObj.audio) { 21354 count++; 21355 } 21356 21357 if (codecObj.video) { 21358 count++; 21359 } 21360 21361 return count; 21362 }; 21363 /** 21364 * Calculates the codec strings for a working configuration of 21365 * SourceBuffers to play variant streams in a main playlist. If 21366 * there is no possible working configuration, an empty object will be 21367 * returned. 21368 * 21369 * @param main {Object} the m3u8 object for the main playlist 21370 * @param media {Object} the m3u8 object for the variant playlist 21371 * @return {Object} the codec strings. 21372 * 21373 * @private 21374 */ 21375 21376 const codecsForPlaylist = function (main, media) { 21377 const mediaAttributes = media.attributes || {}; 21378 const codecInfo = unwrapCodecList(getCodecs(media) || []); // HLS with multiple-audio tracks must always get an audio codec. 21379 // Put another way, there is no way to have a video-only multiple-audio HLS! 21380 21381 if (isMaat(main, media) && !codecInfo.audio) { 21382 if (!isMuxed(main, media)) { 21383 // It is possible for codecs to be specified on the audio media group playlist but 21384 // not on the rendition playlist. This is mostly the case for DASH, where audio and 21385 // video are always separate (and separately specified). 21386 const defaultCodecs = unwrapCodecList(codecsFromDefault(main, mediaAttributes.AUDIO) || []); 21387 21388 if (defaultCodecs.audio) { 21389 codecInfo.audio = defaultCodecs.audio; 21390 } 21391 } 21392 } 21393 21394 return codecInfo; 21395 }; 21396 21397 const logFn = logger('PlaylistSelector'); 21398 21399 const representationToString = function (representation) { 21400 if (!representation || !representation.playlist) { 21401 return; 21402 } 21403 21404 const playlist = representation.playlist; 21405 return JSON.stringify({ 21406 id: playlist.id, 21407 bandwidth: representation.bandwidth, 21408 width: representation.width, 21409 height: representation.height, 21410 codecs: playlist.attributes && playlist.attributes.CODECS || '' 21411 }); 21412 }; // Utilities 21413 21414 /** 21415 * Returns the CSS value for the specified property on an element 21416 * using `getComputedStyle`. Firefox has a long-standing issue where 21417 * getComputedStyle() may return null when running in an iframe with 21418 * `display: none`. 21419 * 21420 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 21421 * @param {HTMLElement} el the htmlelement to work on 21422 * @param {string} the proprety to get the style for 21423 */ 21424 21425 21426 const safeGetComputedStyle = function (el, property) { 21427 if (!el) { 21428 return ''; 21429 } 21430 21431 const result = window.getComputedStyle(el); 21432 21433 if (!result) { 21434 return ''; 21435 } 21436 21437 return result[property]; 21438 }; 21439 /** 21440 * Resuable stable sort function 21441 * 21442 * @param {Playlists} array 21443 * @param {Function} sortFn Different comparators 21444 * @function stableSort 21445 */ 21446 21447 21448 const stableSort = function (array, sortFn) { 21449 const newArray = array.slice(); 21450 array.sort(function (left, right) { 21451 const cmp = sortFn(left, right); 21452 21453 if (cmp === 0) { 21454 return newArray.indexOf(left) - newArray.indexOf(right); 21455 } 21456 21457 return cmp; 21458 }); 21459 }; 21460 /** 21461 * A comparator function to sort two playlist object by bandwidth. 21462 * 21463 * @param {Object} left a media playlist object 21464 * @param {Object} right a media playlist object 21465 * @return {number} Greater than zero if the bandwidth attribute of 21466 * left is greater than the corresponding attribute of right. Less 21467 * than zero if the bandwidth of right is greater than left and 21468 * exactly zero if the two are equal. 21469 */ 21470 21471 21472 const comparePlaylistBandwidth = function (left, right) { 21473 let leftBandwidth; 21474 let rightBandwidth; 21475 21476 if (left.attributes.BANDWIDTH) { 21477 leftBandwidth = left.attributes.BANDWIDTH; 21478 } 21479 21480 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; 21481 21482 if (right.attributes.BANDWIDTH) { 21483 rightBandwidth = right.attributes.BANDWIDTH; 21484 } 21485 21486 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; 21487 return leftBandwidth - rightBandwidth; 21488 }; 21489 /** 21490 * A comparator function to sort two playlist object by resolution (width). 21491 * 21492 * @param {Object} left a media playlist object 21493 * @param {Object} right a media playlist object 21494 * @return {number} Greater than zero if the resolution.width attribute of 21495 * left is greater than the corresponding attribute of right. Less 21496 * than zero if the resolution.width of right is greater than left and 21497 * exactly zero if the two are equal. 21498 */ 21499 21500 const comparePlaylistResolution = function (left, right) { 21501 let leftWidth; 21502 let rightWidth; 21503 21504 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
21505 leftWidth = left.attributes.RESOLUTION.width; 21506 } 21507 21508 leftWidth = leftWidth || window.Number.MAX_VALUE; 21509 21510 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 21511 rightWidth = right.attributes.RESOLUTION.width; 21512 } 21513 21514 rightWidth = rightWidth || window.Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 21515 // have the same media dimensions/ resolution 21516 21517 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 21518 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 21519 } 21520 21521 return leftWidth - rightWidth; 21522 }; 21523 /** 21524 * Chooses the appropriate media playlist based on bandwidth and player size 21525 * 21526 * @param {Object} main 21527 * Object representation of the main manifest 21528 * @param {number} playerBandwidth 21529 * Current calculated bandwidth of the player 21530 * @param {number} playerWidth 21531 * Current width of the player element (should account for the device pixel ratio) 21532 * @param {number} playerHeight 21533 * Current height of the player element (should account for the device pixel ratio) 21534 * @param {boolean} limitRenditionByPlayerDimensions 21535 * True if the player width and height should be used during the selection, false otherwise 21536 * @param {Object} playlistController 21537 * the current playlistController object 21538 * @return {Playlist} the highest bitrate playlist less than the 21539 * currently detected bandwidth, accounting for some amount of 21540 * bandwidth variance 21541 */ 21542 21543 let simpleSelector = function (main, playerBandwidth, playerWidth, playerHeight, limitRenditionByPlayerDimensions, playlistController) { 21544 // If we end up getting called before `main` is available, exit early 21545 if (!main) { 21546 return; 21547 } 21548 21549 const options = { 21550 bandwidth: playerBandwidth, 21551 width: playerWidth,
vendor: 14,579 bytes, lines 21552-21871
21552 height: playerHeight, 21553 limitRenditionByPlayerDimensions 21554 }; 21555 let playlists = main.playlists; // if playlist is audio only, select between currently active audio group playlists. 21556 21557 if (Playlist.isAudioOnly(main)) { 21558 playlists = playlistController.getAudioTrackPlaylists_(); // add audioOnly to options so that we log audioOnly: true 21559 // at the buttom of this function for debugging. 21560 21561 options.audioOnly = true; 21562 } // convert the playlists to an intermediary representation to make comparisons easier 21563 21564 21565 let sortedPlaylistReps = playlists.map(playlist => { 21566 let bandwidth; 21567 const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 21568 const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 21569 bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH; 21570 bandwidth = bandwidth || window.Number.MAX_VALUE; 21571 return { 21572 bandwidth, 21573 width, 21574 height, 21575 playlist 21576 }; 21577 }); 21578 stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); // filter out any playlists that have been excluded due to 21579 // incompatible configurations 21580 21581 sortedPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isIncompatible(rep.playlist)); // filter out any playlists that have been disabled manually through the representations 21582 // api or excluded temporarily due to playback errors. 21583 21584 let enabledPlaylistReps = sortedPlaylistReps.filter(rep => Playlist.isEnabled(rep.playlist)); 21585 21586 if (!enabledPlaylistReps.length) { 21587 // if there are no enabled playlists, then they have all been excluded or disabled 21588 // by the user through the representations api. In this case, ignore exclusion and 21589 // fallback to what the user wants by using playlists the user has not disabled. 21590 enabledPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isDisabled(rep.playlist)); 21591 } // filter out any variant that has greater effective bitrate 21592 // than the current estimated bandwidth 21593 21594 21595 const bandwidthPlaylistReps = enabledPlaylistReps.filter(rep => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth); 21596 let highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth 21597 // and then taking the very first element 21598 21599 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. 21600 21601 if (limitRenditionByPlayerDimensions === false) { 21602 const chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 21603 21604 if (chosenRep && chosenRep.playlist) { 21605 let type = 'sortedPlaylistReps'; 21606 21607 if (bandwidthBestRep) { 21608 type = 'bandwidthBestRep'; 21609 } 21610 21611 if (enabledPlaylistReps[0]) { 21612 type = 'enabledPlaylistReps'; 21613 } 21614 21615 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 21616 return chosenRep.playlist; 21617 } 21618 21619 logFn('could not choose a playlist with options', options); 21620 return null; 21621 } // filter out playlists without resolution information 21622 21623 21624 const haveResolution = bandwidthPlaylistReps.filter(rep => rep.width && rep.height); // sort variants by resolution 21625 21626 stableSort(haveResolution, (left, right) => left.width - right.width); // if we have the exact resolution as the player use it 21627 21628 const resolutionBestRepList = haveResolution.filter(rep => rep.width === playerWidth && rep.height === playerHeight); 21629 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution 21630 21631 const resolutionBestRep = resolutionBestRepList.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; 21632 let resolutionPlusOneList; 21633 let resolutionPlusOneSmallest; 21634 let resolutionPlusOneRep; // find the smallest variant that is larger than the player 21635 // if there is no match of exact resolution 21636 21637 if (!resolutionBestRep) { 21638 resolutionPlusOneList = haveResolution.filter(rep => rep.width > playerWidth || rep.height > playerHeight); // find all the variants have the same smallest resolution 21639 21640 resolutionPlusOneSmallest = resolutionPlusOneList.filter(rep => rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height); // ensure that we also pick the highest bandwidth variant that 21641 // is just-larger-than the video player 21642 21643 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 21644 resolutionPlusOneRep = resolutionPlusOneSmallest.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; 21645 } 21646 21647 let leastPixelDiffRep; // If this selector proves to be better than others, 21648 // resolutionPlusOneRep and resolutionBestRep and all 21649 // the code involving them should be removed. 21650 21651 if (playlistController.leastPixelDiffSelector) { 21652 // find the variant that is closest to the player's pixel size 21653 const leastPixelDiffList = haveResolution.map(rep => { 21654 rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight); 21655 return rep; 21656 }); // get the highest bandwidth, closest resolution playlist 21657 21658 stableSort(leastPixelDiffList, (left, right) => { 21659 // sort by highest bandwidth if pixelDiff is the same 21660 if (left.pixelDiff === right.pixelDiff) { 21661 return right.bandwidth - left.bandwidth; 21662 } 21663 21664 return left.pixelDiff - right.pixelDiff; 21665 }); 21666 leastPixelDiffRep = leastPixelDiffList[0]; 21667 } // fallback chain of variants 21668 21669 21670 const chosenRep = leastPixelDiffRep || resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 21671 21672 if (chosenRep && chosenRep.playlist) { 21673 let type = 'sortedPlaylistReps'; 21674 21675 if (leastPixelDiffRep) { 21676 type = 'leastPixelDiffRep'; 21677 } else if (resolutionPlusOneRep) { 21678 type = 'resolutionPlusOneRep'; 21679 } else if (resolutionBestRep) { 21680 type = 'resolutionBestRep'; 21681 } else if (bandwidthBestRep) { 21682 type = 'bandwidthBestRep'; 21683 } else if (enabledPlaylistReps[0]) { 21684 type = 'enabledPlaylistReps'; 21685 } 21686 21687 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 21688 return chosenRep.playlist; 21689 } 21690 21691 logFn('could not choose a playlist with options', options); 21692 return null; 21693 }; 21694 21695 /** 21696 * Chooses the appropriate media playlist based on the most recent 21697 * bandwidth estimate and the player size. 21698 * 21699 * Expects to be called within the context of an instance of VhsHandler 21700 * 21701 * @return {Playlist} the highest bitrate playlist less than the 21702 * currently detected bandwidth, accounting for some amount of 21703 * bandwidth variance 21704 */ 21705 21706 const lastBandwidthSelector = function () { 21707 const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1; 21708 return simpleSelector(this.playlists.main, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, this.limitRenditionByPlayerDimensions, this.playlistController_); 21709 }; 21710 /** 21711 * Chooses the appropriate media playlist based on an 21712 * exponential-weighted moving average of the bandwidth after 21713 * filtering for player size. 21714 * 21715 * Expects to be called within the context of an instance of VhsHandler 21716 * 21717 * @param {number} decay - a number between 0 and 1. Higher values of 21718 * this parameter will cause previous bandwidth estimates to lose 21719 * significance more quickly. 21720 * @return {Function} a function which can be invoked to create a new 21721 * playlist selector function. 21722 * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 21723 */ 21724 21725 const movingAverageBandwidthSelector = function (decay) { 21726 let average = -1; 21727 let lastSystemBandwidth = -1; 21728 21729 if (decay < 0 || decay > 1) { 21730 throw new Error('Moving average bandwidth decay must be between 0 and 1.'); 21731 } 21732 21733 return function () { 21734 const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1; 21735 21736 if (average < 0) { 21737 average = this.systemBandwidth; 21738 lastSystemBandwidth = this.systemBandwidth; 21739 } // stop the average value from decaying for every 250ms 21740 // when the systemBandwidth is constant 21741 // and 21742 // stop average from setting to a very low value when the 21743 // systemBandwidth becomes 0 in case of chunk cancellation 21744 21745 21746 if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) { 21747 average = decay * this.systemBandwidth + (1 - decay) * average; 21748 lastSystemBandwidth = this.systemBandwidth; 21749 } 21750 21751 return simpleSelector(this.playlists.main, average, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, this.limitRenditionByPlayerDimensions, this.playlistController_); 21752 }; 21753 }; 21754 /** 21755 * Chooses the appropriate media playlist based on the potential to rebuffer 21756 * 21757 * @param {Object} settings 21758 * Object of information required to use this selector 21759 * @param {Object} settings.main 21760 * Object representation of the main manifest 21761 * @param {number} settings.currentTime 21762 * The current time of the player 21763 * @param {number} settings.bandwidth 21764 * Current measured bandwidth 21765 * @param {number} settings.duration 21766 * Duration of the media 21767 * @param {number} settings.segmentDuration 21768 * Segment duration to be used in round trip time calculations 21769 * @param {number} settings.timeUntilRebuffer 21770 * Time left in seconds until the player has to rebuffer 21771 * @param {number} settings.currentTimeline 21772 * The current timeline segments are being loaded from 21773 * @param {SyncController} settings.syncController 21774 * SyncController for determining if we have a sync point for a given playlist 21775 * @return {Object|null} 21776 * {Object} return.playlist 21777 * The highest bandwidth playlist with the least amount of rebuffering 21778 * {Number} return.rebufferingImpact 21779 * The amount of time in seconds switching to this playlist will rebuffer. A 21780 * negative value means that switching will cause zero rebuffering. 21781 */ 21782 21783 const minRebufferMaxBandwidthSelector = function (settings) { 21784 const { 21785 main, 21786 currentTime, 21787 bandwidth, 21788 duration, 21789 segmentDuration, 21790 timeUntilRebuffer, 21791 currentTimeline, 21792 syncController 21793 } = settings; // filter out any playlists that have been excluded due to 21794 // incompatible configurations 21795 21796 const compatiblePlaylists = main.playlists.filter(playlist => !Playlist.isIncompatible(playlist)); // filter out any playlists that have been disabled manually through the representations 21797 // api or excluded temporarily due to playback errors. 21798 21799 let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 21800 21801 if (!enabledPlaylists.length) { 21802 // if there are no enabled playlists, then they have all been excluded or disabled 21803 // by the user through the representations api. In this case, ignore exclusion and 21804 // fallback to what the user wants by using playlists the user has not disabled. 21805 enabledPlaylists = compatiblePlaylists.filter(playlist => !Playlist.isDisabled(playlist)); 21806 } 21807 21808 const bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 21809 const rebufferingEstimates = bandwidthPlaylists.map(playlist => { 21810 const syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a 21811 // sync request first. This will double the request time 21812 21813 const numRequests = syncPoint ? 1 : 2; 21814 const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist); 21815 const rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 21816 return { 21817 playlist, 21818 rebufferingImpact 21819 }; 21820 }); 21821 const noRebufferingPlaylists = rebufferingEstimates.filter(estimate => estimate.rebufferingImpact <= 0); // Sort by bandwidth DESC 21822 21823 stableSort(noRebufferingPlaylists, (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist)); 21824 21825 if (noRebufferingPlaylists.length) { 21826 return noRebufferingPlaylists[0]; 21827 } 21828 21829 stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact); 21830 return rebufferingEstimates[0] || null; 21831 }; 21832 /** 21833 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 21834 * one with video. If no renditions with video exist, return the lowest audio rendition. 21835 * 21836 * Expects to be called within the context of an instance of VhsHandler 21837 * 21838 * @return {Object|null} 21839 * {Object} return.playlist 21840 * The lowest bitrate playlist that contains a video codec. If no such rendition 21841 * exists pick the lowest audio rendition. 21842 */ 21843 21844 const lowestBitrateCompatibleVariantSelector = function () { 21845 // filter out any playlists that have been excluded due to 21846 // incompatible configurations or playback errors 21847 const playlists = this.playlists.main.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate 21848 21849 stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); // Parse and assume that playlists with no video codec have no video 21850 // (this is not necessarily true, although it is generally true). 21851 // 21852 // If an entire manifest has no valid videos everything will get filtered 21853 // out. 21854 21855 const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.main, playlist).video); 21856 return playlistsWithVideo[0] || null; 21857 }; 21858 21859 /** 21860 * Combine all segments into a single Uint8Array 21861 * 21862 * @param {Object} segmentObj 21863 * @return {Uint8Array} concatenated bytes 21864 * @private 21865 */ 21866 const concatSegments = segmentObj => { 21867 let offset = 0; 21868 let tempBuffer; 21869 21870 if (segmentObj.bytes) { 21871 tempBuffer = new Uint8Array(segmentObj.bytes);
21871 // combine the individual segments into one large typed-array 21872 21873 segmentObj.segments.forEach(segment => { 21874 tempBuffer.set(segment, offset); 21875 offset += segment.byteLength; 21876 }); 21877 } 21878 21879 return tempBuffer; 21880 };
vendor: 17,041 bytes, lines 21880-22457
21880 21881 21882 /** 21883 * @file text-tracks.js 21884 */ 21885 /** 21886 * Create captions text tracks on video.js if they do not exist 21887 * 21888 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 21889 * @param {Object} tech the video.js tech 21890 * @param {Object} captionStream the caption stream to create 21891 * @private 21892 */ 21893 21894 const createCaptionsTrackIfNotExists = function (inbandTextTracks, tech, captionStream) { 21895 if (!inbandTextTracks[captionStream]) { 21896 tech.trigger({ 21897 type: 'usage', 21898 name: 'vhs-608' 21899 }); 21900 let instreamId = captionStream; // we need to translate SERVICEn for 708 to how mux.js currently labels them 21901 21902 if (/^cc708_/.test(captionStream)) { 21903 instreamId = 'SERVICE' + captionStream.split('_')[1]; 21904 } 21905 21906 const track = tech.textTracks().getTrackById(instreamId); 21907 21908 if (track) { 21909 // Resuse an existing track with a CC# id because this was 21910 // very likely created by videojs-contrib-hls from information 21911 // in the m3u8 for us to use 21912 inbandTextTracks[captionStream] = track; 21913 } else { 21914 // This section gets called when we have caption services that aren't specified in the manifest. 21915 // Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS. 21916 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {}; 21917 let label = captionStream; 21918 let language = captionStream; 21919 let def = false; 21920 const captionService = captionServices[instreamId]; 21921 21922 if (captionService) { 21923 label = captionService.label; 21924 language = captionService.language; 21925 def = captionService.default; 21926 } // Otherwise, create a track with the default `CC#` label and 21927 // without a language 21928 21929 21930 inbandTextTracks[captionStream] = tech.addRemoteTextTrack({ 21931 kind: 'captions', 21932 id: instreamId, 21933 // TODO: investigate why this doesn't seem to turn the caption on by default 21934 default: def, 21935 label, 21936 language 21937 }, false).track; 21938 } 21939 } 21940 }; 21941 /** 21942 * Add caption text track data to a source handler given an array of captions 21943 * 21944 * @param {Object} 21945 * @param {Object} inbandTextTracks the inband text tracks 21946 * @param {number} timestampOffset the timestamp offset of the source buffer 21947 * @param {Array} captionArray an array of caption data 21948 * @private 21949 */ 21950 21951 const addCaptionData = function ({ 21952 inbandTextTracks, 21953 captionArray, 21954 timestampOffset 21955 }) { 21956 if (!captionArray) { 21957 return; 21958 } 21959 21960 const Cue = window.WebKitDataCue || window.VTTCue; 21961 captionArray.forEach(caption => { 21962 const track = caption.stream; // in CEA 608 captions, video.js/mux.js sends a content array 21963 // with positioning data 21964 21965 if (caption.content) { 21966 caption.content.forEach(value => { 21967 const cue = new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, value.text); 21968 cue.line = value.line; 21969 cue.align = 'left'; 21970 cue.position = value.position; 21971 cue.positionAlign = 'line-left'; 21972 inbandTextTracks[track].addCue(cue); 21973 }); 21974 } else { 21975 // otherwise, a text value with combined captions is sent 21976 inbandTextTracks[track].addCue(new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, caption.text)); 21977 } 21978 }); 21979 }; 21980 /** 21981 * Define properties on a cue for backwards compatability, 21982 * but warn the user that the way that they are using it 21983 * is depricated and will be removed at a later date. 21984 * 21985 * @param {Cue} cue the cue to add the properties on 21986 * @private 21987 */ 21988 21989 const deprecateOldCue = function (cue) { 21990 Object.defineProperties(cue.frame, { 21991 id: { 21992 get() { 21993 videojs__default["default"].log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 21994 return cue.value.key; 21995 } 21996 21997 }, 21998 value: { 21999 get() { 22000 videojs__default["default"].log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 22001 return cue.value.data; 22002 } 22003 22004 }, 22005 privateData: { 22006 get() { 22007 videojs__default["default"].log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 22008 return cue.value.data; 22009 } 22010 22011 } 22012 }); 22013 }; 22014 /** 22015 * Add metadata text track data to a source handler given an array of metadata 22016 * 22017 * @param {Object} 22018 * @param {Object} inbandTextTracks the inband text tracks 22019 * @param {Array} metadataArray an array of meta data 22020 * @param {number} timestampOffset the timestamp offset of the source buffer 22021 * @param {number} videoDuration the duration of the video 22022 * @private 22023 */ 22024 22025 22026 const addMetadata = ({ 22027 inbandTextTracks, 22028 metadataArray, 22029 timestampOffset, 22030 videoDuration 22031 }) => { 22032 if (!metadataArray) { 22033 return; 22034 } 22035 22036 const Cue = window.WebKitDataCue || window.VTTCue; 22037 const metadataTrack = inbandTextTracks.metadataTrack_; 22038 22039 if (!metadataTrack) { 22040 return; 22041 } 22042 22043 metadataArray.forEach(metadata => { 22044 const time = metadata.cueTime + timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN, 22045 // ignore this bit of metadata. 22046 // This likely occurs when you have an non-timed ID3 tag like TIT2, 22047 // which is the "Title/Songname/Content description" frame 22048 22049 if (typeof time !== 'number' || window.isNaN(time) || time < 0 || !(time < Infinity)) { 22050 return; 22051 } // If we have no frames, we can't create a cue. 22052 22053 22054 if (!metadata.frames || !metadata.frames.length) { 22055 return; 22056 } 22057 22058 metadata.frames.forEach(frame => { 22059 const cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 22060 cue.frame = frame; 22061 cue.value = frame; 22062 deprecateOldCue(cue); 22063 metadataTrack.addCue(cue); 22064 }); 22065 }); 22066 22067 if (!metadataTrack.cues || !metadataTrack.cues.length) { 22068 return; 22069 } // Updating the metadeta cues so that 22070 // the endTime of each cue is the startTime of the next cue 22071 // the endTime of last cue is the duration of the video 22072 22073 22074 const cues = metadataTrack.cues; 22075 const cuesArray = []; // Create a copy of the TextTrackCueList... 22076 // ...disregarding cues with a falsey value 22077 22078 for (let i = 0; i < cues.length; i++) { 22079 if (cues[i]) { 22080 cuesArray.push(cues[i]); 22081 } 22082 } // Group cues by their startTime value 22083 22084 22085 const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => { 22086 const timeSlot = obj[cue.startTime] || []; 22087 timeSlot.push(cue); 22088 obj[cue.startTime] = timeSlot; 22089 return obj; 22090 }, {}); // Sort startTimes by ascending order 22091 22092 const sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort((a, b) => Number(a) - Number(b)); // Map each cue group's endTime to the next group's startTime 22093 22094 sortedStartTimes.forEach((startTime, idx) => { 22095 const cueGroup = cuesGroupedByStartTime[startTime]; 22096 const finiteDuration = isFinite(videoDuration) ? videoDuration : startTime; 22097 const nextTime = Number(sortedStartTimes[idx + 1]) || finiteDuration; // Map each cue's endTime the next group's startTime 22098 22099 cueGroup.forEach(cue => { 22100 cue.endTime = nextTime; 22101 }); 22102 }); 22103 }; // object for mapping daterange attributes 22104 22105 const dateRangeAttr = { 22106 id: 'ID', 22107 class: 'CLASS', 22108 startDate: 'START-DATE', 22109 duration: 'DURATION', 22110 endDate: 'END-DATE', 22111 endOnNext: 'END-ON-NEXT', 22112 plannedDuration: 'PLANNED-DURATION', 22113 scte35Out: 'SCTE35-OUT', 22114 scte35In: 'SCTE35-IN' 22115 }; 22116 const dateRangeKeysToOmit = new Set(['id', 'class', 'startDate', 'duration', 'endDate', 'endOnNext', 'startTime', 'endTime', 'processDateRange']); 22117 /** 22118 * Add DateRange metadata text track to a source handler given an array of metadata 22119 * 22120 * @param {Object} 22121 * @param {Object} inbandTextTracks the inband text tracks 22122 * @param {Array} dateRanges parsed media playlist 22123 * @private 22124 */ 22125 22126 const addDateRangeMetadata = ({ 22127 inbandTextTracks, 22128 dateRanges 22129 }) => { 22130 const metadataTrack = inbandTextTracks.metadataTrack_; 22131 22132 if (!metadataTrack) { 22133 return; 22134 } 22135 22136 const Cue = window.WebKitDataCue || window.VTTCue; 22137 dateRanges.forEach(dateRange => { 22138 // we generate multiple cues for each date range with different attributes 22139 for (const key of Object.keys(dateRange)) { 22140 if (dateRangeKeysToOmit.has(key)) { 22141 continue; 22142 } 22143 22144 const cue = new Cue(dateRange.startTime, dateRange.endTime, ''); 22145 cue.id = dateRange.id; 22146 cue.type = 'com.apple.quicktime.HLS'; 22147 cue.value = { 22148 key: dateRangeAttr[key], 22149 data: dateRange[key] 22150 }; 22151 22152 if (key === 'scte35Out' || key === 'scte35In') { 22153 cue.value.data = new Uint8Array(cue.value.data.match(/[\da-f]{2}/gi)).buffer; 22154 } 22155 22156 metadataTrack.addCue(cue); 22157 } 22158 22159 dateRange.processDateRange(); 22160 }); 22161 }; 22162 /** 22163 * Create metadata text track on video.js if it does not exist 22164 * 22165 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 22166 * @param {string} dispatchType the inband metadata track dispatch type 22167 * @param {Object} tech the video.js tech 22168 * @private 22169 */ 22170 22171 const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => { 22172 if (inbandTextTracks.metadataTrack_) { 22173 return; 22174 } 22175 22176 inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack({ 22177 kind: 'metadata', 22178 label: 'Timed Metadata' 22179 }, false).track; 22180 22181 if (!videojs__default["default"].browser.IS_ANY_SAFARI) { 22182 inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType; 22183 } 22184 }; 22185 /** 22186 * Remove cues from a track on video.js. 22187 * 22188 * @param {Double} start start of where we should remove the cue 22189 * @param {Double} end end of where the we should remove the cue 22190 * @param {Object} track the text track to remove the cues from 22191 * @private 22192 */ 22193 22194 const removeCuesFromTrack = function (start, end, track) { 22195 let i; 22196 let cue; 22197 22198 if (!track) { 22199 return; 22200 } 22201 22202 if (!track.cues) { 22203 return; 22204 } 22205 22206 i = track.cues.length; 22207 22208 while (i--) { 22209 cue = track.cues[i]; // Remove any cue within the provided start and end time 22210 22211 if (cue.startTime >= start && cue.endTime <= end) { 22212 track.removeCue(cue); 22213 } 22214 } 22215 }; 22216 /** 22217 * Remove duplicate cues from a track on video.js (a cue is considered a 22218 * duplicate if it has the same time interval and text as another) 22219 * 22220 * @param {Object} track the text track to remove the duplicate cues from 22221 * @private 22222 */ 22223 22224 const removeDuplicateCuesFromTrack = function (track) { 22225 const cues = track.cues; 22226 22227 if (!cues) { 22228 return; 22229 } 22230 22231 const uniqueCues = {}; 22232 22233 for (let i = cues.length - 1; i >= 0; i--) { 22234 const cue = cues[i]; 22235 const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`; 22236 22237 if (uniqueCues[cueKey]) { 22238 track.removeCue(cue); 22239 } else { 22240 uniqueCues[cueKey] = cue; 22241 } 22242 } 22243 }; 22244 22245 /** 22246 * mux.js 22247 * 22248 * Copyright (c) Brightcove 22249 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 22250 */ 22251 var ONE_SECOND_IN_TS = 90000, 22252 // 90kHz clock 22253 secondsToVideoTs, 22254 secondsToAudioTs, 22255 videoTsToSeconds, 22256 audioTsToSeconds, 22257 audioTsToVideoTs, 22258 videoTsToAudioTs, 22259 metadataTsToSeconds; 22260 22261 secondsToVideoTs = function (seconds) { 22262 return seconds * ONE_SECOND_IN_TS; 22263 }; 22264 22265 secondsToAudioTs = function (seconds, sampleRate) { 22266 return seconds * sampleRate; 22267 }; 22268 22269 videoTsToSeconds = function (timestamp) { 22270 return timestamp / ONE_SECOND_IN_TS; 22271 }; 22272 22273 audioTsToSeconds = function (timestamp, sampleRate) { 22274 return timestamp / sampleRate; 22275 }; 22276 22277 audioTsToVideoTs = function (timestamp, sampleRate) { 22278 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 22279 }; 22280 22281 videoTsToAudioTs = function (timestamp, sampleRate) { 22282 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 22283 }; 22284 /** 22285 * Adjust ID3 tag or caption timing information by the timeline pts values 22286 * (if keepOriginalTimestamps is false) and convert to seconds 22287 */ 22288 22289 22290 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 22291 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 22292 }; 22293 22294 var clock = { 22295 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS, 22296 secondsToVideoTs: secondsToVideoTs, 22297 secondsToAudioTs: secondsToAudioTs, 22298 videoTsToSeconds: videoTsToSeconds, 22299 audioTsToSeconds: audioTsToSeconds, 22300 audioTsToVideoTs: audioTsToVideoTs, 22301 videoTsToAudioTs: videoTsToAudioTs, 22302 metadataTsToSeconds: metadataTsToSeconds 22303 }; 22304 22305 /** 22306 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 22307 * front of current time. 22308 * 22309 * @param {Array} buffer 22310 * The current buffer of gop information 22311 * @param {number} currentTime 22312 * The current time 22313 * @param {Double} mapping 22314 * Offset to map display time to stream presentation time 22315 * @return {Array} 22316 * List of gops considered safe to append over 22317 */ 22318 22319 const gopsSafeToAlignWith = (buffer, currentTime, mapping) => { 22320 if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) { 22321 return []; 22322 } // pts value for current time + 3 seconds to give a bit more wiggle room 22323 22324 22325 const currentTimePts = Math.ceil((currentTime - mapping + 3) * clock.ONE_SECOND_IN_TS); 22326 let i; 22327 22328 for (i = 0; i < buffer.length; i++) { 22329 if (buffer[i].pts > currentTimePts) { 22330 break; 22331 } 22332 } 22333 22334 return buffer.slice(i); 22335 }; 22336 /** 22337 * Appends gop information (timing and byteLength) received by the transmuxer for the 22338 * gops appended in the last call to appendBuffer 22339 * 22340 * @param {Array} buffer 22341 * The current buffer of gop information 22342 * @param {Array} gops 22343 * List of new gop information 22344 * @param {boolean} replace 22345 * If true, replace the buffer with the new gop information. If false, append the 22346 * new gop information to the buffer in the right location of time. 22347 * @return {Array} 22348 * Updated list of gop information 22349 */ 22350 22351 const updateGopBuffer = (buffer, gops, replace) => { 22352 if (!gops.length) { 22353 return buffer; 22354 } 22355 22356 if (replace) { 22357 // If we are in safe append mode, then completely overwrite the gop buffer 22358 // with the most recent appeneded data. This will make sure that when appending 22359 // future segments, we only try to align with gops that are both ahead of current 22360 // time and in the last segment appended. 22361 return gops.slice(); 22362 } 22363 22364 const start = gops[0].pts; 22365 let i = 0; 22366 22367 for (i; i < buffer.length; i++) { 22368 if (buffer[i].pts >= start) { 22369 break; 22370 } 22371 } 22372 22373 return buffer.slice(0, i).concat(gops); 22374 }; 22375 /** 22376 * Removes gop information in buffer that overlaps with provided start and end 22377 * 22378 * @param {Array} buffer 22379 * The current buffer of gop information 22380 * @param {Double} start 22381 * position to start the remove at 22382 * @param {Double} end 22383 * position to end the remove at 22384 * @param {Double} mapping 22385 * Offset to map display time to stream presentation time 22386 */ 22387 22388 const removeGopBuffer = (buffer, start, end, mapping) => { 22389 const startPts = Math.ceil((start - mapping) * clock.ONE_SECOND_IN_TS); 22390 const endPts = Math.ceil((end - mapping) * clock.ONE_SECOND_IN_TS); 22391 const updatedBuffer = buffer.slice(); 22392 let i = buffer.length; 22393 22394 while (i--) { 22395 if (buffer[i].pts <= endPts) { 22396 break; 22397 } 22398 } 22399 22400 if (i === -1) { 22401 // no removal because end of remove range is before start of buffer 22402 return updatedBuffer; 22403 } 22404 22405 let j = i + 1; 22406 22407 while (j--) { 22408 if (buffer[j].pts <= startPts) { 22409 break; 22410 } 22411 } // clamp remove range start to 0 index 22412 22413 22414 j = Math.max(j, 0); 22415 updatedBuffer.splice(j, i - j + 1); 22416 return updatedBuffer; 22417 }; 22418 22419 const shallowEqual = function (a, b) { 22420 // if both are undefined 22421 // or one or the other is undefined 22422 // they are not equal 22423 if (!a && !b || !a && b || a && !b) { 22424 return false; 22425 } // they are the same object and thus, equal 22426 22427 22428 if (a === b) { 22429 return true; 22430 } // sort keys so we can make sure they have 22431 // all the same keys later. 22432 22433 22434 const akeys = Object.keys(a).sort(); 22435 const bkeys = Object.keys(b).sort(); // different number of keys, not equal 22436 22437 if (akeys.length !== bkeys.length) { 22438 return false; 22439 } 22440 22441 for (let i = 0; i < akeys.length; i++) { 22442 const key = akeys[i]; // different sorted keys, not equal 22443 22444 if (key !== bkeys[i]) { 22445 return false; 22446 } // different values, not equal 22447 22448 22449 if (a[key] !== b[key]) { 22450 return false; 22451 } 22452 } 22453 22454 return true; 22455 }; 22456 22457 /
22457/ https://www.w3.org/TR/WebIDL-1/#quotaexceedederror 22458 const QUOTA_EXCEEDED_ERR = 22
vendor: 9,776 bytes, lines 22458-22693
22458; 22459 22460 /** 22461 * The segment loader has no recourse except to fetch a segment in the 22462 * current playlist and use the internal timestamps in that segment to 22463 * generate a syncPoint. This function returns a good candidate index 22464 * for that process. 22465 * 22466 * @param {Array} segments - the segments array from a playlist. 22467 * @return {number} An index of a segment from the playlist to load 22468 */ 22469 22470 const getSyncSegmentCandidate = function (currentTimeline, segments, targetTime) { 22471 segments = segments || []; 22472 const timelineSegments = []; 22473 let time = 0; 22474 22475 for (let i = 0; i < segments.length; i++) { 22476 const segment = segments[i]; 22477 22478 if (currentTimeline === segment.timeline) { 22479 timelineSegments.push(i); 22480 time += segment.duration; 22481 22482 if (time > targetTime) { 22483 return i; 22484 } 22485 } 22486 } 22487 22488 if (timelineSegments.length === 0) { 22489 return 0; 22490 } // default to the last timeline segment 22491 22492 22493 return timelineSegments[timelineSegments.length - 1]; 22494 }; // In the event of a quota exceeded error, keep at least one second of back buffer. This 22495 // number was arbitrarily chosen and may be updated in the future, but seemed reasonable 22496 // as a start to prevent any potential issues with removing content too close to the 22497 // playhead. 22498 22499 const MIN_BACK_BUFFER = 1; // in ms 22500 22501 const CHECK_BUFFER_DELAY = 500; 22502 22503 const finite = num => typeof num === 'number' && isFinite(num); // With most content hovering around 30fps, if a segment has a duration less than a half 22504 // frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will 22505 // not accurately reflect the rest of the content. 22506 22507 22508 const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60; 22509 const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => { 22510 // Although these checks should most likely cover non 'main' types, for now it narrows 22511 // the scope of our checks. 22512 if (loaderType !== 'main' || !startingMedia || !trackInfo) { 22513 return null; 22514 } 22515 22516 if (!trackInfo.hasAudio && !trackInfo.hasVideo) { 22517 return 'Neither audio nor video found in segment.'; 22518 } 22519 22520 if (startingMedia.hasVideo && !trackInfo.hasVideo) { 22521 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.'; 22522 } 22523 22524 if (!startingMedia.hasVideo && trackInfo.hasVideo) { 22525 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.'; 22526 } 22527 22528 return null; 22529 }; 22530 /** 22531 * Calculates a time value that is safe to remove from the back buffer without interrupting 22532 * playback. 22533 * 22534 * @param {TimeRange} seekable 22535 * The current seekable range 22536 * @param {number} currentTime 22537 * The current time of the player 22538 * @param {number} targetDuration 22539 * The target duration of the current playlist 22540 * @return {number} 22541 * Time that is safe to remove from the back buffer without interrupting playback 22542 */ 22543 22544 const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => { 22545 // 30 seconds before the playhead provides a safe default for trimming. 22546 // 22547 // Choosing a reasonable default is particularly important for high bitrate content and 22548 // VOD videos/live streams with large windows, as the buffer may end up overfilled and 22549 // throw an APPEND_BUFFER_ERR. 22550 let trimTime = currentTime - Config.BACK_BUFFER_LENGTH; 22551 22552 if (seekable.length) { 22553 // Some live playlists may have a shorter window of content than the full allowed back 22554 // buffer. For these playlists, don't save content that's no longer within the window. 22555 trimTime = Math.max(trimTime, seekable.start(0)); 22556 } // Don't remove within target duration of the current time to avoid the possibility of 22557 // removing the GOP currently being played, as removing it can cause playback stalls. 22558 22559 22560 const maxTrimTime = currentTime - targetDuration; 22561 return Math.min(maxTrimTime, trimTime); 22562 }; 22563 const segmentInfoString = segmentInfo => { 22564 const { 22565 startOfSegment, 22566 duration, 22567 segment, 22568 part, 22569 playlist: { 22570 mediaSequence: seq, 22571 id, 22572 segments = [] 22573 }, 22574 mediaIndex: index, 22575 partIndex, 22576 timeline 22577 } = segmentInfo; 22578 const segmentLen = segments.length - 1; 22579 let selection = 'mediaIndex/partIndex increment'; 22580 22581 if (segmentInfo.getMediaInfoForTime) { 22582 selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`; 22583 } else if (segmentInfo.isSyncRequest) { 22584 selection = 'getSyncSegmentCandidate (isSyncRequest)'; 22585 } 22586 22587 if (segmentInfo.independent) { 22588 selection += ` with independent ${segmentInfo.independent}`; 22589 } 22590 22591 const hasPartIndex = typeof partIndex === 'number'; 22592 const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment'; 22593 const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({ 22594 preloadSegment: segment 22595 }) - 1 : 0; 22596 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}]`; 22597 }; 22598 22599 const timingInfoPropertyForMedia = mediaType => `${mediaType}TimingInfo`; 22600 /** 22601 * Returns the timestamp offset to use for the segment. 22602 * 22603 * @param {number} segmentTimeline 22604 * The timeline of the segment 22605 * @param {number} currentTimeline 22606 * The timeline currently being followed by the loader 22607 * @param {number} startOfSegment 22608 * The estimated segment start 22609 * @param {TimeRange[]} buffered 22610 * The loader's buffer 22611 * @param {boolean} overrideCheck 22612 * If true, no checks are made to see if the timestamp offset value should be set, 22613 * but sets it directly to a value. 22614 * 22615 * @return {number|null} 22616 * Either a number representing a new timestamp offset, or null if the segment is 22617 * part of the same timeline 22618 */ 22619 22620 22621 const timestampOffsetForSegment = ({ 22622 segmentTimeline, 22623 currentTimeline, 22624 startOfSegment, 22625 buffered, 22626 overrideCheck 22627 }) => { 22628 // Check to see if we are crossing a discontinuity to see if we need to set the 22629 // timestamp offset on the transmuxer and source buffer. 22630 // 22631 // Previously, we changed the timestampOffset if the start of this segment was less than 22632 // the currently set timestampOffset, but this isn't desirable as it can produce bad 22633 // behavior, especially around long running live streams. 22634 if (!overrideCheck && segmentTimeline === currentTimeline) { 22635 return null; 22636 } // When changing renditions, it's possible to request a segment on an older timeline. For 22637 // instance, given two renditions with the following: 22638 // 22639 // #EXTINF:10 22640 // segment1 22641 // #EXT-X-DISCONTINUITY 22642 // #EXTINF:10 22643 // segment2 22644 // #EXTINF:10 22645 // segment3 22646 // 22647 // And the current player state: 22648 // 22649 // current time: 8 22650 // buffer: 0 => 20 22651 // 22652 // The next segment on the current rendition would be segment3, filling the buffer from 22653 // 20s onwards. However, if a rendition switch happens after segment2 was requested, 22654 // then the next segment to be requested will be segment1 from the new rendition in 22655 // order to fill time 8 and onwards. Using the buffered end would result in repeated 22656 // content (since it would position segment1 of the new rendition starting at 20s). This 22657 // case can be identified when the new segment's timeline is a prior value. Instead of 22658 // using the buffered end, the startOfSegment can be used, which, hopefully, will be 22659 // more accurate to the actual start time of the segment. 22660 22661 22662 if (segmentTimeline < currentTimeline) { 22663 return startOfSegment; 22664 } // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that 22665 // value uses the end of the last segment if it is available. While this value 22666 // should often be correct, it's better to rely on the buffered end, as the new 22667 // content post discontinuity should line up with the buffered end as if it were 22668 // time 0 for the new content. 22669 22670 22671 return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment; 22672 }; 22673 /** 22674 * Returns whether or not the loader should wait for a timeline change from the timeline 22675 * change controller before processing the segment. 22676 * 22677 * Primary timing in VHS goes by video. This is different from most media players, as 22678 * audio is more often used as the primary timing source. For the foreseeable future, VHS 22679 * will continue to use video as the primary timing source, due to the current logic and 22680 * expectations built around it. 22681 22682 * Since the timing follows video, in order to maintain sync, the video loader is 22683 * responsible for setting both audio and video source buffer timestamp offsets. 22684 * 22685 * Setting different values for audio and video source buffers could lead to 22686 * desyncing. The following examples demonstrate some of the situations where this 22687 * distinction is important. Note that all of these cases involve demuxed content. When 22688 * content is muxed, the audio and video are packaged together, therefore syncing 22689 * separate media playlists is not an issue. 22690 * 22691 * CASE 1: Audio prepares to load a new timeline before video: 22692 * 22693 * Timeline: 0 1
vendor: 2,320 bytes, lines 22694-22734
22694 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 22695 * Audio Loader: ^ 22696 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 22697 * Video Loader ^ 22698 * 22699 * In the above example, the audio loader is preparing to load the 6th segment, the first 22700 * after a discontinuity, while the video loader is still loading the 5th segment, before 22701 * the discontinuity. 22702 * 22703 * If the audio loader goes ahead and loads and appends the 6th segment before the video 22704 * loader crosses the discontinuity, then when appended, the 6th audio segment will use 22705 * the timestamp offset from timeline 0. This will likely lead to desyncing. In addition, 22706 * the audio loader must provide the audioAppendStart value to trim the content in the 22707 * transmuxer, and that value relies on the audio timestamp offset. Since the audio 22708 * timestamp offset is set by the video (main) loader, the audio loader shouldn't load the 22709 * segment until that value is provided. 22710 * 22711 * CASE 2: Video prepares to load a new timeline before audio: 22712 * 22713 * Timeline: 0 1 22714 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 22715 * Audio Loader: ^ 22716 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 22717 * Video Loader ^ 22718 * 22719 * In the above example, the video loader is preparing to load the 6th segment, the first 22720 * after a discontinuity, while the audio loader is still loading the 5th segment, before 22721 * the discontinuity. 22722 * 22723 * If the video loader goes ahead and loads and appends the 6th segment, then once the 22724 * segment is loaded and processed, both the video and audio timestamp offsets will be 22725 * set, since video is used as the primary timing source. This is to ensure content lines 22726 * up appropriately, as any modifications to the video timing are reflected by audio when 22727 * the video loader sets the audio and video timestamp offsets to the same value. However, 22728 * setting the timestamp offset for audio before audio has had a chance to change 22729 * timelines will likely lead to desyncing, as the audio loader will append segment 5 with 22730 * a timestamp intended to apply to segments from timeline 1 rather than timeline 0. 22731 * 22732 * CASE 3: When seeking, audio prepares to load a new timeline before video 22733 * 22734 * Timeline: 0 1
vendor: 4,331 bytes, lines 22735-22826
22735 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 22736 * Audio Loader: ^ 22737 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 22738 * Video Loader ^ 22739 * 22740 * In the above example, both audio and video loaders are loading segments from timeline 22741 * 0, but imagine that the seek originated from timeline 1. 22742 * 22743 * When seeking to a new timeline, the timestamp offset will be set based on the expected 22744 * segment start of the loaded video segment. In order to maintain sync, the audio loader 22745 * must wait for the video loader to load its segment and update both the audio and video 22746 * timestamp offsets before it may load and append its own segment. This is the case 22747 * whether the seek results in a mismatched segment request (e.g., the audio loader 22748 * chooses to load segment 3 and the video loader chooses to load segment 4) or the 22749 * loaders choose to load the same segment index from each playlist, as the segments may 22750 * not be aligned perfectly, even for matching segment indexes. 22751 * 22752 * @param {Object} timelinechangeController 22753 * @param {number} currentTimeline 22754 * The timeline currently being followed by the loader 22755 * @param {number} segmentTimeline 22756 * The timeline of the segment being loaded 22757 * @param {('main'|'audio')} loaderType 22758 * The loader type 22759 * @param {boolean} audioDisabled 22760 * Whether the audio is disabled for the loader. This should only be true when the 22761 * loader may have muxed audio in its segment, but should not append it, e.g., for 22762 * the main loader when an alternate audio playlist is active. 22763 * 22764 * @return {boolean} 22765 * Whether the loader should wait for a timeline change from the timeline change 22766 * controller before processing the segment 22767 */ 22768 22769 const shouldWaitForTimelineChange = ({ 22770 timelineChangeController, 22771 currentTimeline, 22772 segmentTimeline, 22773 loaderType, 22774 audioDisabled 22775 }) => { 22776 if (currentTimeline === segmentTimeline) { 22777 return false; 22778 } 22779 22780 if (loaderType === 'audio') { 22781 const lastMainTimelineChange = timelineChangeController.lastTimelineChange({ 22782 type: 'main' 22783 }); // Audio loader should wait if: 22784 // 22785 // * main hasn't had a timeline change yet (thus has not loaded its first segment) 22786 // * main hasn't yet changed to the timeline audio is looking to load 22787 22788 return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline; 22789 } // The main loader only needs to wait for timeline changes if there's demuxed audio. 22790 // Otherwise, there's nothing to wait for, since audio would be muxed into the main 22791 // loader's segments (or the content is audio/video only and handled by the main 22792 // loader). 22793 22794 22795 if (loaderType === 'main' && audioDisabled) { 22796 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({ 22797 type: 'audio' 22798 }); // Main loader should wait for the audio loader if audio is not pending a timeline 22799 // change to the current timeline. 22800 // 22801 // Since the main loader is responsible for setting the timestamp offset for both 22802 // audio and video, the main loader must wait for audio to be about to change to its 22803 // timeline before setting the offset, otherwise, if audio is behind in loading, 22804 // segments from the previous timeline would be adjusted by the new timestamp offset. 22805 // 22806 // This requirement means that video will not cross a timeline until the audio is 22807 // about to cross to it, so that way audio and video will always cross the timeline 22808 // together. 22809 // 22810 // In addition to normal timeline changes, these rules also apply to the start of a 22811 // stream (going from a non-existent timeline, -1, to timeline 0). It's important 22812 // that these rules apply to the first timeline change because if they did not, it's 22813 // possible that the main loader will cross two timelines before the audio loader has 22814 // crossed one. Logic may be implemented to handle the startup as a special case, but 22815 // it's easier to simply treat all timeline changes the same. 22816 22817 if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) { 22818 return false; 22819 } 22820 22821 return true; 22822 } 22823 22824 return false; 22825 }; 22826 const
vendor: 5,914 bytes, lines 22826-22983
22826mediaDuration = timingInfos => { 22827 let maxDuration = 0; 22828 ['video', 'audio'].forEach(function (type) { 22829 const typeTimingInfo = timingInfos[`${type}TimingInfo`]; 22830 22831 if (!typeTimingInfo) { 22832 return; 22833 } 22834 22835 const { 22836 start, 22837 end 22838 } = typeTimingInfo; 22839 let duration; 22840 22841 if (typeof start === 'bigint' || typeof end === 'bigint') { 22842 duration = window.BigInt(end) - window.BigInt(start); 22843 } else if (typeof start === 'number' && typeof end === 'number') { 22844 duration = end - start; 22845 } 22846 22847 if (typeof duration !== 'undefined' && duration > maxDuration) { 22848 maxDuration = duration; 22849 } 22850 }); // convert back to a number if it is lower than MAX_SAFE_INTEGER 22851 // as we only need BigInt when we are above that. 22852 22853 if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) { 22854 maxDuration = Number(maxDuration); 22855 } 22856 22857 return maxDuration; 22858 }; 22859 const segmentTooLong = ({ 22860 segmentDuration, 22861 maxDuration 22862 }) => { 22863 // 0 duration segments are most likely due to metadata only segments or a lack of 22864 // information. 22865 if (!segmentDuration) { 22866 return false; 22867 } // For HLS: 22868 // 22869 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1 22870 // The EXTINF duration of each Media Segment in the Playlist 22871 // file, when rounded to the nearest integer, MUST be less than or equal 22872 // to the target duration; longer segments can trigger playback stalls 22873 // or other errors. 22874 // 22875 // For DASH, the mpd-parser uses the largest reported segment duration as the target 22876 // duration. Although that reported duration is occasionally approximate (i.e., not 22877 // exact), a strict check may report that a segment is too long more often in DASH. 22878 22879 22880 return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR; 22881 }; 22882 const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => { 22883 // Right now we aren't following DASH's timing model exactly, so only perform 22884 // this check for HLS content. 22885 if (sourceType !== 'hls') { 22886 return null; 22887 } 22888 22889 const segmentDuration = mediaDuration({ 22890 audioTimingInfo: segmentInfo.audioTimingInfo, 22891 videoTimingInfo: segmentInfo.videoTimingInfo 22892 }); // Don't report if we lack information. 22893 // 22894 // If the segment has a duration of 0 it is either a lack of information or a 22895 // metadata only segment and shouldn't be reported here. 22896 22897 if (!segmentDuration) { 22898 return null; 22899 } 22900 22901 const targetDuration = segmentInfo.playlist.targetDuration; 22902 const isSegmentWayTooLong = segmentTooLong({ 22903 segmentDuration, 22904 maxDuration: targetDuration * 2 22905 }); 22906 const isSegmentSlightlyTooLong = segmentTooLong({ 22907 segmentDuration, 22908 maxDuration: targetDuration 22909 }); 22910 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'; 22911 22912 if (isSegmentWayTooLong || isSegmentSlightlyTooLong) { 22913 return { 22914 severity: isSegmentWayTooLong ? 'warn' : 'info', 22915 message: segmentTooLongMessage 22916 }; 22917 } 22918 22919 return null; 22920 }; 22921 /** 22922 * An object that manages segment loading and appending. 22923 * 22924 * @class SegmentLoader 22925 * @param {Object} options required and optional options 22926 * @extends videojs.EventTarget 22927 */ 22928 22929 class SegmentLoader extends videojs__default["default"].EventTarget { 22930 constructor(settings, options = {}) { 22931 super(); // check pre-conditions 22932 22933 if (!settings) { 22934 throw new TypeError('Initialization settings are required'); 22935 } 22936 22937 if (typeof settings.currentTime !== 'function') { 22938 throw new TypeError('No currentTime getter specified'); 22939 } 22940 22941 if (!settings.mediaSource) { 22942 throw new TypeError('No MediaSource specified'); 22943 } // public properties 22944 22945 22946 this.bandwidth = settings.bandwidth; 22947 this.throughput = { 22948 rate: 0, 22949 count: 0 22950 }; 22951 this.roundTrip = NaN; 22952 this.resetStats_(); 22953 this.mediaIndex = null; 22954 this.partIndex = null; // private settings 22955 22956 this.hasPlayed_ = settings.hasPlayed; 22957 this.currentTime_ = settings.currentTime; 22958 this.seekable_ = settings.seekable; 22959 this.seeking_ = settings.seeking; 22960 this.duration_ = settings.duration; 22961 this.mediaSource_ = settings.mediaSource; 22962 this.vhs_ = settings.vhs; 22963 this.loaderType_ = settings.loaderType; 22964 this.currentMediaInfo_ = void 0; 22965 this.startingMediaInfo_ = void 0; 22966 this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 22967 this.goalBufferLength_ = settings.goalBufferLength; 22968 this.sourceType_ = settings.sourceType; 22969 this.sourceUpdater_ = settings.sourceUpdater; 22970 this.inbandTextTracks_ = settings.inbandTextTracks; 22971 this.state_ = 'INIT'; 22972 this.timelineChangeController_ = settings.timelineChangeController; 22973 this.shouldSaveSegmentTimingInfo_ = true; 22974 this.parse708captions_ = settings.parse708captions; 22975 this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset; 22976 this.captionServices_ = settings.captionServices; 22977 this.exactManifestTimings = settings.exactManifestTimings; 22978 this.addMetadataToTextTrack = settings.addMetadataToTextTrack; // private instance variables 22979 22980 this.checkBufferTimeout_ = null; 22981 this.error_ = void 0; 22982 this.currentTimeline_ = -1; 22983 this.shouldForceTimestampOffsetAfterResync_ = false;
vendor: 5,534 bytes, lines 22984-23163
22984 this.pendingSegment_ = null; 22985 this.xhrOptions_ = null; 22986 this.pendingSegments_ = []; 22987 this.audioDisabled_ = false; 22988 this.isPendingTimestampOffset_ = false; // TODO possibly move gopBuffer and timeMapping info to a separate controller 22989 22990 this.gopBuffer_ = []; 22991 this.timeMapping_ = 0; 22992 this.safeAppend_ = false; 22993 this.appendInitSegment_ = { 22994 audio: true, 22995 video: true 22996 }; 22997 this.playlistOfLastInitSegment_ = { 22998 audio: null, 22999 video: null 23000 }; 23001 this.callQueue_ = []; // If the segment loader prepares to load a segment, but does not have enough 23002 // information yet to start the loading process (e.g., if the audio loader wants to 23003 // load a segment from the next timeline but the main loader hasn't yet crossed that 23004 // timeline), then the load call will be added to the queue until it is ready to be 23005 // processed. 23006 23007 this.loadQueue_ = []; 23008 this.metadataQueue_ = { 23009 id3: [], 23010 caption: [] 23011 }; 23012 this.waitingOnRemove_ = false; 23013 this.quotaExceededErrorRetryTimeout_ = null; // Fragmented mp4 playback 23014 23015 this.activeInitSegmentId_ = null; 23016 this.initSegments_ = {}; // HLSe playback 23017 23018 this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys; 23019 this.keyCache_ = {}; 23020 this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings 23021 // between a time in the display time and a segment index within 23022 // a playlist 23023 23024 this.syncController_ = settings.syncController; 23025 this.syncPoint_ = { 23026 segmentIndex: 0, 23027 time: 0 23028 }; 23029 this.transmuxer_ = this.createTransmuxer_(); 23030 23031 this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate'); 23032 23033 this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_); 23034 this.mediaSource_.addEventListener('sourceopen', () => { 23035 if (!this.isEndOfStream_()) { 23036 this.ended_ = false; 23037 } 23038 }); // ...for determining the fetch location 23039 23040 this.fetchAtBuffer_ = false; 23041 this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`); 23042 Object.defineProperty(this, 'state', { 23043 get() { 23044 return this.state_; 23045 }, 23046 23047 set(newState) { 23048 if (newState !== this.state_) { 23049 this.logger_(`${this.state_} -> ${newState}`); 23050 this.state_ = newState; 23051 this.trigger('statechange'); 23052 } 23053 } 23054 23055 }); 23056 this.sourceUpdater_.on('ready', () => { 23057 if (this.hasEnoughInfoToAppend_()) { 23058 this.processCallQueue_(); 23059 } 23060 }); // Only the main loader needs to listen for pending timeline changes, as the main 23061 // loader should wait for audio to be ready to change its timeline so that both main 23062 // and audio timelines change together. For more details, see the 23063 // shouldWaitForTimelineChange function. 23064 23065 if (this.loaderType_ === 'main') { 23066 this.timelineChangeController_.on('pendingtimelinechange', () => { 23067 if (this.hasEnoughInfoToAppend_()) { 23068 this.processCallQueue_(); 23069 } 23070 }); 23071 } // The main loader only listens on pending timeline changes, but the audio loader, 23072 // since its loads follow main, needs to listen on timeline changes. For more details, 23073 // see the shouldWaitForTimelineChange function. 23074 23075 23076 if (this.loaderType_ === 'audio') { 23077 this.timelineChangeController_.on('timelinechange', () => { 23078 if (this.hasEnoughInfoToLoad_()) { 23079 this.processLoadQueue_(); 23080 } 23081 23082 if (this.hasEnoughInfoToAppend_()) { 23083 this.processCallQueue_(); 23084 } 23085 }); 23086 } 23087 } 23088 23089 createTransmuxer_() { 23090 return segmentTransmuxer.createTransmuxer({ 23091 remux: false, 23092 alignGopsAtEnd: this.safeAppend_, 23093 keepOriginalTimestamps: true, 23094 parse708captions: this.parse708captions_, 23095 captionServices: this.captionServices_ 23096 }); 23097 } 23098 /** 23099 * reset all of our media stats 23100 * 23101 * @private 23102 */ 23103 23104 23105 resetStats_() { 23106 this.mediaBytesTransferred = 0; 23107 this.mediaRequests = 0; 23108 this.mediaRequestsAborted = 0; 23109 this.mediaRequestsTimedout = 0; 23110 this.mediaRequestsErrored = 0; 23111 this.mediaTransferDuration = 0; 23112 this.mediaSecondsLoaded = 0; 23113 this.mediaAppends = 0; 23114 } 23115 /** 23116 * dispose of the SegmentLoader and reset to the default state 23117 */ 23118 23119 23120 dispose() { 23121 this.trigger('dispose'); 23122 this.state = 'DISPOSED'; 23123 this.pause(); 23124 this.abort_(); 23125 23126 if (this.transmuxer_) { 23127 this.transmuxer_.terminate(); 23128 } 23129 23130 this.resetStats_(); 23131 23132 if (this.checkBufferTimeout_) { 23133 window.clearTimeout(this.checkBufferTimeout_); 23134 } 23135 23136 if (this.syncController_ && this.triggerSyncInfoUpdate_) { 23137 this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_); 23138 } 23139 23140 this.off(); 23141 } 23142 23143 setAudio(enable) { 23144 this.audioDisabled_ = !enable; 23145 23146 if (enable) { 23147 this.appendInitSegment_.audio = true; 23148 } else { 23149 // remove current track audio if it gets disabled 23150 this.sourceUpdater_.removeAudio(0, this.duration_()); 23151 } 23152 } 23153 /** 23154 * abort anything that is currently doing on with the SegmentLoader 23155 * and reset to a default state 23156 */ 23157 23158 23159 abort() { 23160 if (this.state !== 'WAITING') { 23161 if (this.pendingSegment_) { 23162 this.pendingSegment_ = null; 23163 }
vendor: 6,936 bytes, lines 23164-23419
23164 23165 return; 23166 } 23167 23168 this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY 23169 // since we are no longer "waiting" on any requests. XHR callback is not always run 23170 // when the request is aborted. This will prevent the loader from being stuck in the 23171 // WAITING state indefinitely. 23172 23173 this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the 23174 // next segment 23175 23176 if (!this.paused()) { 23177 this.monitorBuffer_(); 23178 } 23179 } 23180 /** 23181 * abort all pending xhr requests and null any pending segements 23182 * 23183 * @private 23184 */ 23185 23186 23187 abort_() { 23188 if (this.pendingSegment_ && this.pendingSegment_.abortRequests) { 23189 this.pendingSegment_.abortRequests(); 23190 } // clear out the segment being processed 23191 23192 23193 this.pendingSegment_ = null; 23194 this.callQueue_ = []; 23195 this.loadQueue_ = []; 23196 this.metadataQueue_.id3 = []; 23197 this.metadataQueue_.caption = []; 23198 this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_); 23199 this.waitingOnRemove_ = false; 23200 window.clearTimeout(this.quotaExceededErrorRetryTimeout_); 23201 this.quotaExceededErrorRetryTimeout_ = null; 23202 } 23203 23204 checkForAbort_(requestId) { 23205 // If the state is APPENDING, then aborts will not modify the state, meaning the first 23206 // callback that happens should reset the state to READY so that loading can continue. 23207 if (this.state === 'APPENDING' && !this.pendingSegment_) { 23208 this.state = 'READY'; 23209 return true; 23210 } 23211 23212 if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) { 23213 return true; 23214 } 23215 23216 return false; 23217 } 23218 /** 23219 * set an error on the segment loader and null out any pending segements 23220 * 23221 * @param {Error} error the error to set on the SegmentLoader 23222 * @return {Error} the error that was set or that is currently set 23223 */ 23224 23225 23226 error(error) { 23227 if (typeof error !== 'undefined') { 23228 this.logger_('error occurred:', error); 23229 this.error_ = error; 23230 } 23231 23232 this.pendingSegment_ = null; 23233 return this.error_; 23234 } 23235 23236 endOfStream() { 23237 this.ended_ = true; 23238 23239 if (this.transmuxer_) { 23240 // need to clear out any cached data to prepare for the new segment 23241 segmentTransmuxer.reset(this.transmuxer_); 23242 } 23243 23244 this.gopBuffer_.length = 0; 23245 this.pause(); 23246 this.trigger('ended'); 23247 } 23248 /** 23249 * Indicates which time ranges are buffered 23250 * 23251 * @return {TimeRange} 23252 * TimeRange object representing the current buffered ranges 23253 */ 23254 23255 23256 buffered_() { 23257 const trackInfo = this.getMediaInfo_(); 23258 23259 if (!this.sourceUpdater_ || !trackInfo) { 23260 return createTimeRanges(); 23261 } 23262 23263 if (this.loaderType_ === 'main') { 23264 const { 23265 hasAudio, 23266 hasVideo, 23267 isMuxed 23268 } = trackInfo; 23269 23270 if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) { 23271 return this.sourceUpdater_.buffered(); 23272 } 23273 23274 if (hasVideo) { 23275 return this.sourceUpdater_.videoBuffered(); 23276 } 23277 } // One case that can be ignored for now is audio only with alt audio, 23278 // as we don't yet have proper support for that. 23279 23280 23281 return this.sourceUpdater_.audioBuffered(); 23282 } 23283 /** 23284 * Gets and sets init segment for the provided map 23285 * 23286 * @param {Object} map 23287 * The map object representing the init segment to get or set 23288 * @param {boolean=} set 23289 * If true, the init segment for the provided map should be saved 23290 * @return {Object} 23291 * map object for desired init segment 23292 */ 23293 23294 23295 initSegmentForMap(map, set = false) { 23296 if (!map) { 23297 return null; 23298 } 23299 23300 const id = initSegmentId(map); 23301 let storedMap = this.initSegments_[id]; 23302 23303 if (set && !storedMap && map.bytes) { 23304 this.initSegments_[id] = storedMap = { 23305 resolvedUri: map.resolvedUri, 23306 byterange: map.byterange, 23307 bytes: map.bytes, 23308 tracks: map.tracks, 23309 timescales: map.timescales 23310 }; 23311 } 23312 23313 return storedMap || map; 23314 } 23315 /** 23316 * Gets and sets key for the provided key 23317 * 23318 * @param {Object} key 23319 * The key object representing the key to get or set 23320 * @param {boolean=} set 23321 * If true, the key for the provided key should be saved 23322 * @return {Object} 23323 * Key object for desired key 23324 */ 23325 23326 23327 segmentKey(key, set = false) { 23328 if (!key) { 23329 return null; 23330 } 23331 23332 const id = segmentKeyId(key); 23333 let storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per 23334 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3 23335 23336 if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) { 23337 this.keyCache_[id] = storedKey = { 23338 resolvedUri: key.resolvedUri, 23339 bytes: key.bytes 23340 }; 23341 } 23342 23343 const result = { 23344 resolvedUri: (storedKey || key).resolvedUri 23345 }; 23346 23347 if (storedKey) { 23348 result.bytes = storedKey.bytes; 23349 } 23350 23351 return result; 23352 } 23353 /** 23354 * Returns true if all configuration required for loading is present, otherwise false. 23355 * 23356 * @return {boolean} True if the all configuration is ready for loading 23357 * @private 23358 */ 23359 23360 23361 couldBeginLoading_() { 23362 return this.playlist_ && !this.paused(); 23363 } 23364 /** 23365 * load a playlist and start to fill the buffer 23366 */ 23367 23368 23369 load() { 23370 // un-pause 23371 this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be 23372 // specified 23373 23374 if (!this.playlist_) { 23375 return; 23376 } // if all the configuration is ready, initialize and begin loading 23377 23378 23379 if (this.state === 'INIT' && this.couldBeginLoading_()) { 23380 return this.init_(); 23381 } // if we're in the middle of processing a segment already, don't 23382 // kick off an additional segment request 23383 23384 23385 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') { 23386 return; 23387 } 23388 23389 this.state = 'READY'; 23390 } 23391 /** 23392 * Once all the starting parameters have been specified, begin 23393 * operation. This method should only be invoked from the INIT 23394 * state. 23395 * 23396 * @private 23397 */ 23398 23399 23400 init_() { 23401 this.state = 'READY'; // if this is the audio segment loader, and it hasn't been inited before, then any old 23402 // audio data from the muxed content should be removed 23403 23404 this.resetEverything(); 23405 return this.monitorBuffer_(); 23406 } 23407 /** 23408 * set a playlist on the segment loader 23409 * 23410 * @param {PlaylistLoader} media the playlist to set on the segment loader 23411 */ 23412 23413 23414 playlist(newPlaylist, options = {}) { 23415 if (!newPlaylist) { 23416 return; 23417 } 23418 23419
vendor: 29,711 bytes, lines 23419-24171
23419const oldPlaylist = this.playlist_; 23420 const segmentInfo = this.pendingSegment_; 23421 this.playlist_ = newPlaylist; 23422 this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist 23423 // is always our zero-time so force a sync update each time the playlist 23424 // is refreshed from the server 23425 // 23426 // Use the INIT state to determine if playback has started, as the playlist sync info 23427 // should be fixed once requests begin (as sync points are generated based on sync 23428 // info), but not before then. 23429 23430 if (this.state === 'INIT') { 23431 newPlaylist.syncInfo = { 23432 mediaSequence: newPlaylist.mediaSequence, 23433 time: 0 23434 }; // Setting the date time mapping means mapping the program date time (if available) 23435 // to time 0 on the player's timeline. The playlist's syncInfo serves a similar 23436 // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can 23437 // be updated as the playlist is refreshed before the loader starts loading, the 23438 // program date time mapping needs to be updated as well. 23439 // 23440 // This mapping is only done for the main loader because a program date time should 23441 // map equivalently between playlists. 23442 23443 if (this.loaderType_ === 'main') { 23444 this.syncController_.setDateTimeMappingForStart(newPlaylist); 23445 } 23446 } 23447 23448 let oldId = null; 23449 23450 if (oldPlaylist) { 23451 if (oldPlaylist.id) { 23452 oldId = oldPlaylist.id; 23453 } else if (oldPlaylist.uri) { 23454 oldId = oldPlaylist.uri; 23455 } 23456 } 23457 23458 this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`); 23459 this.syncController_.updateMediaSequenceMap(newPlaylist, this.currentTime_(), this.loaderType_); // in VOD, this is always a rendition switch (or we updated our syncInfo above) 23460 // in LIVE, we always want to update with new playlists (including refreshes) 23461 23462 this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start 23463 // buffering now 23464 23465 if (this.state === 'INIT' && this.couldBeginLoading_()) { 23466 return this.init_(); 23467 } 23468 23469 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 23470 if (this.mediaIndex !== null) { 23471 // we must reset/resync the segment loader when we switch renditions and 23472 // the segment loader is already synced to the previous rendition 23473 // We only want to reset the loader here for LLHLS playback, as resetLoader sets fetchAtBuffer_ 23474 // to false, resulting in fetching segments at currentTime and causing repeated 23475 // same-segment requests on playlist change. This erroneously drives up the playback watcher 23476 // stalled segment count, as re-requesting segments at the currentTime or browser cached segments 23477 // will not change the buffer. 23478 // Reference for LLHLS fixes: https://github.com/videojs/http-streaming/pull/1201 23479 const isLLHLS = !newPlaylist.endList && typeof newPlaylist.partTargetDuration === 'number'; 23480 23481 if (isLLHLS) { 23482 this.resetLoader(); 23483 } else { 23484 this.resyncLoader(); 23485 } 23486 } 23487 23488 this.currentMediaInfo_ = void 0; 23489 this.trigger('playlistupdate'); // the rest of this function depends on `oldPlaylist` being defined 23490 23491 return; 23492 } // we reloaded the same playlist so we are in a live scenario 23493 // and we will likely need to adjust the mediaIndex 23494 23495 23496 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 23497 this.logger_(`live window shift [${mediaSequenceDiff}]`); // update the mediaIndex on the SegmentLoader 23498 // this is important because we can abort a request and this value must be 23499 // equal to the last appended mediaIndex 23500 23501 if (this.mediaIndex !== null) { 23502 this.mediaIndex -= mediaSequenceDiff; // this can happen if we are going to load the first segment, but get a playlist 23503 // update during that. mediaIndex would go from 0 to -1 if mediaSequence in the 23504 // new playlist was incremented by 1. 23505 23506 if (this.mediaIndex < 0) { 23507 this.mediaIndex = null; 23508 this.partIndex = null; 23509 } else { 23510 const segment = this.playlist_.segments[this.mediaIndex]; // partIndex should remain the same for the same segment 23511 // unless parts fell off of the playlist for this segment. 23512 // In that case we need to reset partIndex and resync 23513 23514 if (this.partIndex && (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex])) { 23515 const mediaIndex = this.mediaIndex; 23516 this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`); 23517 this.resetLoader(); // We want to throw away the partIndex and the data associated with it, 23518 // as the part was dropped from our current playlists segment. 23519 // The mediaIndex will still be valid so keep that around. 23520 23521 this.mediaIndex = mediaIndex; 23522 } 23523 } 23524 } // update the mediaIndex on the SegmentInfo object 23525 // this is important because we will update this.mediaIndex with this value 23526 // in `handleAppendsDone_` after the segment has been successfully appended 23527 23528 23529 if (segmentInfo) { 23530 segmentInfo.mediaIndex -= mediaSequenceDiff; 23531 23532 if (segmentInfo.mediaIndex < 0) { 23533 segmentInfo.mediaIndex = null; 23534 segmentInfo.partIndex = null; 23535 } else { 23536 // we need to update the referenced segment so that timing information is 23537 // saved for the new playlist's segment, however, if the segment fell off the 23538 // playlist, we can leave the old reference and just lose the timing info 23539 if (segmentInfo.mediaIndex >= 0) { 23540 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 23541 } 23542 23543 if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) { 23544 segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex]; 23545 } 23546 } 23547 } 23548 23549 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 23550 } 23551 /** 23552 * Prevent the loader from fetching additional segments. If there 23553 * is a segment request outstanding, it will finish processing 23554 * before the loader halts. A segment loader can be unpaused by 23555 * calling load(). 23556 */ 23557 23558 23559 pause() { 23560 if (this.checkBufferTimeout_) { 23561 window.clearTimeout(this.checkBufferTimeout_); 23562 this.checkBufferTimeout_ = null; 23563 } 23564 } 23565 /** 23566 * Returns whether the segment loader is fetching additional 23567 * segments when given the opportunity. This property can be 23568 * modified through calls to pause() and load(). 23569 */ 23570 23571 23572 paused() { 23573 return this.checkBufferTimeout_ === null; 23574 } 23575 /** 23576 * Delete all the buffered data and reset the SegmentLoader 23577 * 23578 * @param {Function} [done] an optional callback to be executed when the remove 23579 * operation is complete 23580 */ 23581 23582 23583 resetEverything(done) { 23584 this.ended_ = false; 23585 this.activeInitSegmentId_ = null; 23586 this.appendInitSegment_ = { 23587 audio: true, 23588 video: true 23589 }; 23590 this.resetLoader(); // remove from 0, the earliest point, to Infinity, to signify removal of everything. 23591 // VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader, 23592 // we then clamp the value to duration if necessary. 23593 23594 this.remove(0, Infinity, done); // clears fmp4 captions 23595 23596 if (this.transmuxer_) { 23597 this.transmuxer_.postMessage({ 23598 action: 'clearAllMp4Captions' 23599 }); // reset the cache in the transmuxer 23600 23601 this.transmuxer_.postMessage({ 23602 action: 'reset' 23603 }); 23604 } 23605 } 23606 /** 23607 * Force the SegmentLoader to resync and start loading around the currentTime instead 23608 * of starting at the end of the buffer 23609 * 23610 * Useful for fast quality changes 23611 */ 23612 23613 23614 resetLoader() { 23615 this.fetchAtBuffer_ = false; 23616 this.resyncLoader(); 23617 } 23618 /** 23619 * Force the SegmentLoader to restart synchronization and make a conservative guess 23620 * before returning to the simple walk-forward method 23621 */ 23622 23623 23624 resyncLoader() { 23625 if (this.transmuxer_) { 23626 // need to clear out any cached data to prepare for the new segment 23627 segmentTransmuxer.reset(this.transmuxer_); 23628 } 23629 23630 this.mediaIndex = null; 23631 this.partIndex = null; 23632 this.syncPoint_ = null; 23633 this.isPendingTimestampOffset_ = false; 23634 this.shouldForceTimestampOffsetAfterResync_ = true; 23635 this.callQueue_ = []; 23636 this.loadQueue_ = []; 23637 this.metadataQueue_.id3 = []; 23638 this.metadataQueue_.caption = []; 23639 this.abort(); 23640 23641 if (this.transmuxer_) { 23642 this.transmuxer_.postMessage({ 23643 action: 'clearParsedMp4Captions' 23644 }); 23645 } 23646 } 23647 /** 23648 * Remove any data in the source buffer between start and end times 23649 * 23650 * @param {number} start - the start time of the region to remove from the buffer 23651 * @param {number} end - the end time of the region to remove from the buffer 23652 * @param {Function} [done] - an optional callback to be executed when the remove 23653 * @param {boolean} force - force all remove operations to happen 23654 * operation is complete 23655 */ 23656 23657 23658 remove(start, end, done = () => {}, force = false) { 23659 // clamp end to duration if we need to remove everything. 23660 // This is due to a browser bug that causes issues if we remove to Infinity. 23661 // videojs/videojs-contrib-hls#1225 23662 if (end === Infinity) { 23663 end = this.duration_(); 23664 } // skip removes that would throw an error 23665 // commonly happens during a rendition switch at the start of a video 23666 // from start 0 to end 0 23667 23668 23669 if (end <= start) { 23670 this.logger_('skipping remove because end ${end} is <= start ${start}'); 23671 return; 23672 } 23673 23674 if (!this.sourceUpdater_ || !this.getMediaInfo_()) { 23675 this.logger_('skipping remove because no source updater or starting media info'); // nothing to remove if we haven't processed any media 23676 23677 return; 23678 } // set it to one to complete this function's removes 23679 23680 23681 let removesRemaining = 1; 23682 23683 const removeFinished = () => { 23684 removesRemaining--; 23685 23686 if (removesRemaining === 0) { 23687 done(); 23688 } 23689 }; 23690 23691 if (force || !this.audioDisabled_) { 23692 removesRemaining++; 23693 this.sourceUpdater_.removeAudio(start, end, removeFinished); 23694 } // While it would be better to only remove video if the main loader has video, this 23695 // should be safe with audio only as removeVideo will call back even if there's no 23696 // video buffer. 23697 // 23698 // In theory we can check to see if there's video before calling the remove, but in 23699 // the event that we're switching between renditions and from video to audio only 23700 // (when we add support for that), we may need to clear the video contents despite 23701 // what the new media will contain. 23702 23703 23704 if (force || this.loaderType_ === 'main') { 23705 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 23706 removesRemaining++; 23707 this.sourceUpdater_.removeVideo(start, end, removeFinished); 23708 } // remove any captions and ID3 tags 23709 23710 23711 for (const track in this.inbandTextTracks_) { 23712 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 23713 } 23714 23715 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); // finished this function's removes 23716 23717 removeFinished(); 23718 } 23719 /** 23720 * (re-)schedule monitorBufferTick_ to run as soon as possible 23721 * 23722 * @private 23723 */ 23724 23725 23726 monitorBuffer_() { 23727 if (this.checkBufferTimeout_) { 23728 window.clearTimeout(this.checkBufferTimeout_); 23729 } 23730 23731 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1); 23732 } 23733 /** 23734 * As long as the SegmentLoader is in the READY state, periodically 23735 * invoke fillBuffer_(). 23736 * 23737 * @private 23738 */ 23739 23740 23741 monitorBufferTick_() { 23742 if (this.state === 'READY') { 23743 this.fillBuffer_(); 23744 } 23745 23746 if (this.checkBufferTimeout_) { 23747 window.clearTimeout(this.checkBufferTimeout_); 23748 } 23749 23750 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY); 23751 } 23752 /** 23753 * fill the buffer with segements unless the sourceBuffers are 23754 * currently updating 23755 * 23756 * Note: this function should only ever be called by monitorBuffer_ 23757 * and never directly 23758 * 23759 * @private 23760 */ 23761 23762 23763 fillBuffer_() { 23764 // TODO since the source buffer maintains a queue, and we shouldn't call this function 23765 // except when we're ready for the next segment, this check can most likely be removed 23766 if (this.sourceUpdater_.updating()) { 23767 return; 23768 } // see if we need to begin loading immediately 23769 23770 23771 const segmentInfo = this.chooseNextRequest_(); 23772 23773 if (!segmentInfo) { 23774 return; 23775 } 23776 23777 if (typeof segmentInfo.timestampOffset === 'number') { 23778 this.isPendingTimestampOffset_ = false; 23779 this.timelineChangeController_.pendingTimelineChange({ 23780 type: this.loaderType_, 23781 from: this.currentTimeline_, 23782 to: segmentInfo.timeline 23783 }); 23784 } 23785 23786 this.loadSegment_(segmentInfo); 23787 } 23788 /** 23789 * Determines if we should call endOfStream on the media source based 23790 * on the state of the buffer or if appened segment was the final 23791 * segment in the playlist. 23792 * 23793 * @param {number} [mediaIndex] the media index of segment we last appended 23794 * @param {Object} [playlist] a media playlist object 23795 * @return {boolean} do we need to call endOfStream on the MediaSource 23796 */ 23797 23798 23799 isEndOfStream_(mediaIndex = this.mediaIndex, playlist = this.playlist_, partIndex = this.partIndex) { 23800 if (!playlist || !this.mediaSource_) { 23801 return false; 23802 } 23803 23804 const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex]; // mediaIndex is zero based but length is 1 based 23805 23806 const appendedLastSegment = mediaIndex + 1 === playlist.segments.length; // true if there are no parts, or this is the last part. 23807 23808 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 23809 // so that MediaSources can trigger the `ended` event when it runs out of 23810 // buffered data instead of waiting for me 23811 23812 return playlist.endList && this.mediaSource_.readyState === 'open' && appendedLastSegment && appendedLastPart; 23813 } 23814 /** 23815 * Determines what request should be made given current segment loader state. 23816 * 23817 * @return {Object} a request object that describes the segment/part to load 23818 */ 23819 23820 23821 chooseNextRequest_() { 23822 const buffered = this.buffered_(); 23823 const bufferedEnd = lastBufferedEnd(buffered) || 0; 23824 const bufferedTime = timeAheadOf(buffered, this.currentTime_()); 23825 const preloaded = !this.hasPlayed_() && bufferedTime >= 1; 23826 const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_(); 23827 const segments = this.playlist_.segments; // return no segment if: 23828 // 1. we don't have segments 23829 // 2. The video has not yet played and we already downloaded a segment 23830 // 3. we already have enough buffered time 23831 23832 if (!segments.length || preloaded || haveEnoughBuffer) { 23833 return null; 23834 } 23835 23836 this.syncPoint_ = this.syncPoint_ || this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_(), this.loaderType_); 23837 const next = { 23838 partIndex: null, 23839 mediaIndex: null, 23840 startOfSegment: null, 23841 playlist: this.playlist_, 23842 isSyncRequest: Boolean(!this.syncPoint_) 23843 }; 23844 23845 if (next.isSyncRequest) { 23846 next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd); 23847 this.logger_(`choose next request. Can not find sync point. Fallback to media Index: ${next.mediaIndex}`); 23848 } else if (this.mediaIndex !== null) { 23849 const segment = segments[this.mediaIndex]; 23850 const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1; 23851 next.startOfSegment = segment.end ? segment.end : bufferedEnd; 23852 23853 if (segment.parts && segment.parts[partIndex + 1]) { 23854 next.mediaIndex = this.mediaIndex; 23855 next.partIndex = partIndex + 1; 23856 } else { 23857 next.mediaIndex = this.mediaIndex + 1; 23858 } 23859 } else { 23860 // Find the segment containing the end of the buffer or current time. 23861 const { 23862 segmentIndex, 23863 startTime, 23864 partIndex 23865 } = Playlist.getMediaInfoForTime({ 23866 exactManifestTimings: this.exactManifestTimings, 23867 playlist: this.playlist_, 23868 currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(), 23869 startingPartIndex: this.syncPoint_.partIndex, 23870 startingSegmentIndex: this.syncPoint_.segmentIndex, 23871 startTime: this.syncPoint_.time 23872 }); 23873 next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`; 23874 next.mediaIndex = segmentIndex; 23875 next.startOfSegment = startTime; 23876 next.partIndex = partIndex; 23877 this.logger_(`choose next request. Playlist switched and we have a sync point. Media Index: ${next.mediaIndex} `); 23878 } 23879 23880 const nextSegment = segments[next.mediaIndex]; 23881 let nextPart = nextSegment && typeof next.partIndex === 'number' && nextSegment.parts && nextSegment.parts[next.partIndex]; // if the next segment index is invalid or 23882 // the next partIndex is invalid do not choose a next segment. 23883 23884 if (!nextSegment || typeof next.partIndex === 'number' && !nextPart) { 23885 return null; 23886 } // if the next segment has parts, and we don't have a partIndex. 23887 // Set partIndex to 0 23888 23889 23890 if (typeof next.partIndex !== 'number' && nextSegment.parts) { 23891 next.partIndex = 0; 23892 nextPart = nextSegment.parts[0]; 23893 } // independentSegments applies to every segment in a playlist. If independentSegments appears in a main playlist, 23894 // it applies to each segment in each media playlist. 23895 // https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.5.1 23896 23897 23898 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 23899 // that the next part we append is "independent" if possible. 23900 // So we check if the previous part is independent, and request 23901 // it if it is. 23902 23903 if (!bufferedTime && nextPart && !hasIndependentSegments && !nextPart.independent) { 23904 if (next.partIndex === 0) { 23905 const lastSegment = segments[next.mediaIndex - 1]; 23906 const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1]; 23907 23908 if (lastSegmentLastPart && lastSegmentLastPart.independent) { 23909 next.mediaIndex -= 1; 23910 next.partIndex = lastSegment.parts.length - 1; 23911 next.independent = 'previous segment'; 23912 } 23913 } else if (nextSegment.parts[next.partIndex - 1].independent) { 23914 next.partIndex -= 1; 23915 next.independent = 'previous part'; 23916 } 23917 } 23918 23919 const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended'; // do not choose a next segment if all of the following: 23920 // 1. this is the last segment in the playlist 23921 // 2. end of stream has been called on the media source already 23922 // 3. the player is not seeking 23923 23924 if (next.mediaIndex >= segments.length - 1 && ended && !this.seeking_()) { 23925 return null; 23926 } 23927 23928 if (this.shouldForceTimestampOffsetAfterResync_) { 23929 this.shouldForceTimestampOffsetAfterResync_ = false; 23930 next.forceTimestampOffset = true; 23931 this.logger_('choose next request. Force timestamp offset after loader resync'); 23932 } 23933 23934 return this.generateSegmentInfo_(next); 23935 } 23936 23937 generateSegmentInfo_(options) { 23938 const { 23939 independent, 23940 playlist, 23941 mediaIndex, 23942 startOfSegment, 23943 isSyncRequest, 23944 partIndex, 23945 forceTimestampOffset, 23946 getMediaInfoForTime 23947 } = options; 23948 const segment = playlist.segments[mediaIndex]; 23949 const part = typeof partIndex === 'number' && segment.parts[partIndex]; 23950 const segmentInfo = { 23951 requestId: 'segment-loader-' + Math.random(), 23952 // resolve the segment URL relative to the playlist 23953 uri: part && part.resolvedUri || segment.resolvedUri, 23954 // the segment's mediaIndex at the time it was requested 23955 mediaIndex, 23956 partIndex: part ? partIndex : null, 23957 // whether or not to update the SegmentLoader's state with this 23958 // segment's mediaIndex 23959 isSyncRequest, 23960 startOfSegment, 23961 // the segment's playlist 23962 playlist, 23963 // unencrypted bytes of the segment 23964 bytes: null, 23965 // when a key is defined for this segment, the encrypted bytes 23966 encryptedBytes: null, 23967 // The target timestampOffset for this segment when we append it 23968 // to the source buffer 23969 timestampOffset: null, 23970 // The timeline that the segment is in 23971 timeline: segment.timeline, 23972 // The expected duration of the segment in seconds 23973 duration: part && part.duration || segment.duration, 23974 // retain the segment in case the playlist updates while doing an async process 23975 segment, 23976 part, 23977 byteLength: 0, 23978 transmuxer: this.transmuxer_, 23979 // type of getMediaInfoForTime that was used to get this segment 23980 getMediaInfoForTime, 23981 independent 23982 }; 23983 const overrideCheck = typeof forceTimestampOffset !== 'undefined' ? forceTimestampOffset : this.isPendingTimestampOffset_; 23984 segmentInfo.timestampOffset = this.timestampOffsetForSegment_({ 23985 segmentTimeline: segment.timeline, 23986 currentTimeline: this.currentTimeline_, 23987 startOfSegment, 23988 buffered: this.buffered_(), 23989 overrideCheck 23990 }); 23991 const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered()); 23992 23993 if (typeof audioBufferedEnd === 'number') { 23994 // since the transmuxer is using the actual timing values, but the buffer is 23995 // adjusted by the timestamp offset, we must adjust the value here 23996 segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset(); 23997 } 23998 23999 if (this.sourceUpdater_.videoBuffered().length) { 24000 segmentInfo.gopsToAlignWith = gopsSafeToAlignWith(this.gopBuffer_, // since the transmuxer is using the actual timing values, but the time is 24001 // adjusted by the timestmap offset, we must adjust the value here 24002 this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(), this.timeMapping_); 24003 } 24004 24005 return segmentInfo; 24006 } // get the timestampoffset for a segment, 24007 // added so that vtt segment loader can override and prevent 24008 // adding timestamp offsets. 24009 24010 24011 timestampOffsetForSegment_(options) { 24012 return timestampOffsetForSegment(options); 24013 } 24014 /** 24015 * Determines if the network has enough bandwidth to complete the current segment 24016 * request in a timely manner. If not, the request will be aborted early and bandwidth 24017 * updated to trigger a playlist switch. 24018 * 24019 * @param {Object} stats 24020 * Object containing stats about the request timing and size 24021 * @private 24022 */ 24023 24024 24025 earlyAbortWhenNeeded_(stats) { 24026 if (this.vhs_.tech_.paused() || // Don't abort if the current playlist is on the lowestEnabledRendition 24027 // TODO: Replace using timeout with a boolean indicating whether this playlist is 24028 // the lowestEnabledRendition. 24029 !this.xhrOptions_.timeout || // Don't abort if we have no bandwidth information to estimate segment sizes 24030 !this.playlist_.attributes.BANDWIDTH) { 24031 return; 24032 } // Wait at least 1 second since the first byte of data has been received before 24033 // using the calculated bandwidth from the progress event to allow the bitrate 24034 // to stabilize 24035 24036 24037 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 24038 return; 24039 } 24040 24041 const currentTime = this.currentTime_(); 24042 const measuredBandwidth = stats.bandwidth; 24043 const segmentDuration = this.pendingSegment_.duration; 24044 const requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 24045 // if we are only left with less than 1 second when the request completes. 24046 // A negative timeUntilRebuffering indicates we are already rebuffering 24047 24048 const timeUntilRebuffer$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.vhs_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download 24049 // is larger than the estimated time until the player runs out of forward buffer 24050 24051 if (requestTimeRemaining <= timeUntilRebuffer$1) { 24052 return; 24053 } 24054 24055 const switchCandidate = minRebufferMaxBandwidthSelector({ 24056 main: this.vhs_.playlists.main, 24057 currentTime, 24058 bandwidth: measuredBandwidth, 24059 duration: this.duration_(), 24060 segmentDuration, 24061 timeUntilRebuffer: timeUntilRebuffer$1, 24062 currentTimeline: this.currentTimeline_, 24063 syncController: this.syncController_ 24064 }); 24065 24066 if (!switchCandidate) { 24067 return; 24068 } 24069 24070 const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$1; 24071 const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 24072 let minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the 24073 // potential round trip time of the new request so that we are not too aggressive 24074 // with switching to a playlist that might save us a fraction of a second. 24075 24076 if (timeUntilRebuffer$1 <= TIME_FUDGE_FACTOR) { 24077 minimumTimeSaving = 1; 24078 } 24079 24080 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) { 24081 return; 24082 } // set the bandwidth to that of the desired playlist being sure to scale by 24083 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 24084 // don't trigger a bandwidthupdate as the bandwidth is artifial 24085 24086 24087 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 24088 this.trigger('earlyabort'); 24089 } 24090 24091 handleAbort_(segmentInfo) { 24092 this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`); 24093 this.mediaRequestsAborted += 1; 24094 } 24095 /** 24096 * XHR `progress` event handler 24097 * 24098 * @param {Event} 24099 * The XHR `progress` event 24100 * @param {Object} simpleSegment 24101 * A simplified segment object copy 24102 * @private 24103 */ 24104 24105 24106 handleProgress_(event, simpleSegment) { 24107 this.earlyAbortWhenNeeded_(simpleSegment.stats); 24108 24109 if (this.checkForAbort_(simpleSegment.requestId)) { 24110 return; 24111 } 24112 24113 this.trigger('progress'); 24114 } 24115 24116 handleTrackInfo_(simpleSegment, trackInfo) { 24117 this.earlyAbortWhenNeeded_(simpleSegment.stats); 24118 24119 if (this.checkForAbort_(simpleSegment.requestId)) { 24120 return; 24121 } 24122 24123 if (this.checkForIllegalMediaSwitch(trackInfo)) { 24124 return; 24125 } 24126 24127 trackInfo = trackInfo || {}; // When we have track info, determine what media types this loader is dealing with. 24128 // Guard against cases where we're not getting track info at all until we are 24129 // certain that all streams will provide it. 24130 24131 if (!shallowEqual(this.currentMediaInfo_, trackInfo)) { 24132 this.appendInitSegment_ = { 24133 audio: true, 24134 video: true 24135 }; 24136 this.startingMediaInfo_ = trackInfo; 24137 this.currentMediaInfo_ = trackInfo; 24138 this.logger_('trackinfo update', trackInfo); 24139 this.trigger('trackinfo'); 24140 } // trackinfo may cause an abort if the trackinfo 24141 // causes a codec change to an unsupported codec. 24142 24143 24144 if (this.checkForAbort_(simpleSegment.requestId)) { 24145 return; 24146 } // set trackinfo on the pending segment so that 24147 // it can append. 24148 24149 24150 this.pendingSegment_.trackInfo = trackInfo; // check if any calls were waiting on the track info 24151 24152 if (this.hasEnoughInfoToAppend_()) { 24153 this.processCallQueue_(); 24154 } 24155 } 24156 24157 handleTimingInfo_(simpleSegment, mediaType, timeType, time) { 24158 this.earlyAbortWhenNeeded_(simpleSegment.stats); 24159 24160 if (this.checkForAbort_(simpleSegment.requestId)) { 24161 return; 24162 } 24163 24164 const segmentInfo = this.pendingSegment_; 24165 const timingInfoProperty = timingInfoPropertyForMedia(mediaType); 24166 segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {}; 24167 segmentInfo[timingInfoProperty][timeType] = time; 24168 this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`); // check if any calls were waiting on the timing info 24169 24170 if (this.hasEnoughInfoToAppend_()) { 24171 this.processCallQueue_(
vendor: 8,530 bytes, lines 24171-24388
24171); 24172 } 24173 } 24174 24175 handleCaptions_(simpleSegment, captionData) { 24176 this.earlyAbortWhenNeeded_(simpleSegment.stats); 24177 24178 if (this.checkForAbort_(simpleSegment.requestId)) { 24179 return; 24180 } // This could only happen with fmp4 segments, but 24181 // should still not happen in general 24182 24183 24184 if (captionData.length === 0) { 24185 this.logger_('SegmentLoader received no captions from a caption event'); 24186 return; 24187 } 24188 24189 const segmentInfo = this.pendingSegment_; // Wait until we have some video data so that caption timing 24190 // can be adjusted by the timestamp offset 24191 24192 if (!segmentInfo.hasAppendedData_) { 24193 this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData)); 24194 return; 24195 } 24196 24197 const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset(); 24198 const captionTracks = {}; // get total start/end and captions for each track/stream 24199 24200 captionData.forEach(caption => { 24201 // caption.stream is actually a track name... 24202 // set to the existing values in tracks or default values 24203 captionTracks[caption.stream] = captionTracks[caption.stream] || { 24204 // Infinity, as any other value will be less than this 24205 startTime: Infinity, 24206 captions: [], 24207 // 0 as an other value will be more than this 24208 endTime: 0 24209 }; 24210 const captionTrack = captionTracks[caption.stream]; 24211 captionTrack.startTime = Math.min(captionTrack.startTime, caption.startTime + timestampOffset); 24212 captionTrack.endTime = Math.max(captionTrack.endTime, caption.endTime + timestampOffset); 24213 captionTrack.captions.push(caption); 24214 }); 24215 Object.keys(captionTracks).forEach(trackName => { 24216 const { 24217 startTime, 24218 endTime, 24219 captions 24220 } = captionTracks[trackName]; 24221 const inbandTextTracks = this.inbandTextTracks_; 24222 this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`); 24223 createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName); // clear out any cues that start and end at the same time period for the same track. 24224 // We do this because a rendition change that also changes the timescale for captions 24225 // will result in captions being re-parsed for certain segments. If we add them again 24226 // without clearing we will have two of the same captions visible. 24227 24228 removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]); 24229 addCaptionData({ 24230 captionArray: captions, 24231 inbandTextTracks, 24232 timestampOffset 24233 }); 24234 }); // Reset stored captions since we added parsed 24235 // captions to a text track at this point 24236 24237 if (this.transmuxer_) { 24238 this.transmuxer_.postMessage({ 24239 action: 'clearParsedMp4Captions' 24240 }); 24241 } 24242 } 24243 24244 handleId3_(simpleSegment, id3Frames, dispatchType) { 24245 this.earlyAbortWhenNeeded_(simpleSegment.stats); 24246 24247 if (this.checkForAbort_(simpleSegment.requestId)) { 24248 return; 24249 } 24250 24251 const segmentInfo = this.pendingSegment_; // we need to have appended data in order for the timestamp offset to be set 24252 24253 if (!segmentInfo.hasAppendedData_) { 24254 this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType)); 24255 return; 24256 } 24257 24258 this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_()); 24259 } 24260 24261 processMetadataQueue_() { 24262 this.metadataQueue_.id3.forEach(fn => fn()); 24263 this.metadataQueue_.caption.forEach(fn => fn()); 24264 this.metadataQueue_.id3 = []; 24265 this.metadataQueue_.caption = []; 24266 } 24267 24268 processCallQueue_() { 24269 const callQueue = this.callQueue_; // Clear out the queue before the queued functions are run, since some of the 24270 // functions may check the length of the load queue and default to pushing themselves 24271 // back onto the queue. 24272 24273 this.callQueue_ = []; 24274 callQueue.forEach(fun => fun()); 24275 } 24276 24277 processLoadQueue_() { 24278 const loadQueue = this.loadQueue_; // Clear out the queue before the queued functions are run, since some of the 24279 // functions may check the length of the load queue and default to pushing themselves 24280 // back onto the queue. 24281 24282 this.loadQueue_ = []; 24283 loadQueue.forEach(fun => fun()); 24284 } 24285 /** 24286 * Determines whether the loader has enough info to load the next segment. 24287 * 24288 * @return {boolean} 24289 * Whether or not the loader has enough info to load the next segment 24290 */ 24291 24292 24293 hasEnoughInfoToLoad_() { 24294 // Since primary timing goes by video, only the audio loader potentially needs to wait 24295 // to load. 24296 if (this.loaderType_ !== 'audio') { 24297 return true; 24298 } 24299 24300 const segmentInfo = this.pendingSegment_; // A fill buffer must have already run to establish a pending segment before there's 24301 // enough info to load. 24302 24303 if (!segmentInfo) { 24304 return false; 24305 } // The first segment can and should be loaded immediately so that source buffers are 24306 // created together (before appending). Source buffer creation uses the presence of 24307 // audio and video data to determine whether to create audio/video source buffers, and 24308 // uses processed (transmuxed or parsed) media to determine the types required. 24309 24310 24311 if (!this.getCurrentMediaInfo_()) { 24312 return true; 24313 } 24314 24315 if ( // Technically, instead of waiting to load a segment on timeline changes, a segment 24316 // can be requested and downloaded and only wait before it is transmuxed or parsed. 24317 // But in practice, there are a few reasons why it is better to wait until a loader 24318 // is ready to append that segment before requesting and downloading: 24319 // 24320 // 1. Because audio and main loaders cross discontinuities together, if this loader 24321 // is waiting for the other to catch up, then instead of requesting another 24322 // segment and using up more bandwidth, by not yet loading, more bandwidth is 24323 // allotted to the loader currently behind. 24324 // 2. media-segment-request doesn't have to have logic to consider whether a segment 24325 // is ready to be processed or not, isolating the queueing behavior to the loader. 24326 // 3. The audio loader bases some of its segment properties on timing information 24327 // provided by the main loader, meaning that, if the logic for waiting on 24328 // processing was in media-segment-request, then it would also need to know how 24329 // to re-generate the segment information after the main loader caught up. 24330 shouldWaitForTimelineChange({ 24331 timelineChangeController: this.timelineChangeController_, 24332 currentTimeline: this.currentTimeline_, 24333 segmentTimeline: segmentInfo.timeline, 24334 loaderType: this.loaderType_, 24335 audioDisabled: this.audioDisabled_ 24336 })) { 24337 return false; 24338 } 24339 24340 return true; 24341 } 24342 24343 getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) { 24344 return segmentInfo && segmentInfo.trackInfo || this.currentMediaInfo_; 24345 } 24346 24347 getMediaInfo_(segmentInfo = this.pendingSegment_) { 24348 return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_; 24349 } 24350 24351 getPendingSegmentPlaylist() { 24352 return this.pendingSegment_ ? this.pendingSegment_.playlist : null; 24353 } 24354 24355 hasEnoughInfoToAppend_() { 24356 if (!this.sourceUpdater_.ready()) { 24357 return false; 24358 } // If content needs to be removed or the loader is waiting on an append reattempt, 24359 // then no additional content should be appended until the prior append is resolved. 24360 24361 24362 if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) { 24363 return false; 24364 } 24365 24366 const segmentInfo = this.pendingSegment_; 24367 const trackInfo = this.getCurrentMediaInfo_(); // no segment to append any data for or 24368 // we do not have information on this specific 24369 // segment yet 24370 24371 if (!segmentInfo || !trackInfo) { 24372 return false; 24373 } 24374 24375 const { 24376 hasAudio, 24377 hasVideo, 24378 isMuxed 24379 } = trackInfo; 24380 24381 if (hasVideo && !segmentInfo.videoTimingInfo) { 24382 return false; 24383 } // muxed content only relies on video timing information for now. 24384 24385 24386 if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) { 24387 return false; 24388 }
vendor: 24,875 bytes, lines 24388-24979
24388 24389 24390 if (shouldWaitForTimelineChange({ 24391 timelineChangeController: this.timelineChangeController_, 24392 currentTimeline: this.currentTimeline_, 24393 segmentTimeline: segmentInfo.timeline, 24394 loaderType: this.loaderType_, 24395 audioDisabled: this.audioDisabled_ 24396 })) { 24397 return false; 24398 } 24399 24400 return true; 24401 } 24402 24403 handleData_(simpleSegment, result) { 24404 this.earlyAbortWhenNeeded_(simpleSegment.stats); 24405 24406 if (this.checkForAbort_(simpleSegment.requestId)) { 24407 return; 24408 } // If there's anything in the call queue, then this data came later and should be 24409 // executed after the calls currently queued. 24410 24411 24412 if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) { 24413 this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result)); 24414 return; 24415 } 24416 24417 const segmentInfo = this.pendingSegment_; // update the time mapping so we can translate from display time to media time 24418 24419 this.setTimeMapping_(segmentInfo.timeline); // for tracking overall stats 24420 24421 this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment); // Note that the state isn't changed from loading to appending. This is because abort 24422 // logic may change behavior depending on the state, and changing state too early may 24423 // inflate our estimates of bandwidth. In the future this should be re-examined to 24424 // note more granular states. 24425 // don't process and append data if the mediaSource is closed 24426 24427 if (this.mediaSource_.readyState === 'closed') { 24428 return; 24429 } // if this request included an initialization segment, save that data 24430 // to the initSegment cache 24431 24432 24433 if (simpleSegment.map) { 24434 simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true); // move over init segment properties to media request 24435 24436 segmentInfo.segment.map = simpleSegment.map; 24437 } // if this request included a segment key, save that data in the cache 24438 24439 24440 if (simpleSegment.key) { 24441 this.segmentKey(simpleSegment.key, true); 24442 } 24443 24444 segmentInfo.isFmp4 = simpleSegment.isFmp4; 24445 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 24446 24447 if (segmentInfo.isFmp4) { 24448 this.trigger('fmp4'); 24449 segmentInfo.timingInfo.start = segmentInfo[timingInfoPropertyForMedia(result.type)].start; 24450 } else { 24451 const trackInfo = this.getCurrentMediaInfo_(); 24452 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 24453 let firstVideoFrameTimeForData; 24454 24455 if (useVideoTimingInfo) { 24456 firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start; 24457 } // Segment loader knows more about segment timing than the transmuxer (in certain 24458 // aspects), so make any changes required for a more accurate start time. 24459 // Don't set the end time yet, as the segment may not be finished processing. 24460 24461 24462 segmentInfo.timingInfo.start = this.trueSegmentStart_({ 24463 currentStart: segmentInfo.timingInfo.start, 24464 playlist: segmentInfo.playlist, 24465 mediaIndex: segmentInfo.mediaIndex, 24466 currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(), 24467 useVideoTimingInfo, 24468 firstVideoFrameTimeForData, 24469 videoTimingInfo: segmentInfo.videoTimingInfo, 24470 audioTimingInfo: segmentInfo.audioTimingInfo 24471 }); 24472 } // Init segments for audio and video only need to be appended in certain cases. Now 24473 // that data is about to be appended, we can check the final cases to determine 24474 // whether we should append an init segment. 24475 24476 24477 this.updateAppendInitSegmentStatus(segmentInfo, result.type); // Timestamp offset should be updated once we get new data and have its timing info, 24478 // as we use the start of the segment to offset the best guess (playlist provided) 24479 // timestamp offset. 24480 24481 this.updateSourceBufferTimestampOffset_(segmentInfo); // if this is a sync request we need to determine whether it should 24482 // be appended or not. 24483 24484 if (segmentInfo.isSyncRequest) { 24485 // first save/update our timing info for this segment. 24486 // this is what allows us to choose an accurate segment 24487 // and the main reason we make a sync request. 24488 this.updateTimingInfoEnd_(segmentInfo); 24489 this.syncController_.saveSegmentTimingInfo({ 24490 segmentInfo, 24491 shouldSaveTimelineMapping: this.loaderType_ === 'main' 24492 }); 24493 const next = this.chooseNextRequest_(); // If the sync request isn't the segment that would be requested next 24494 // after taking into account its timing info, do not append it. 24495 24496 if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) { 24497 this.logger_('sync segment was incorrect, not appending'); 24498 return; 24499 } // otherwise append it like any other segment as our guess was correct. 24500 24501 24502 this.logger_('sync segment was correct, appending'); 24503 } // Save some state so that in the future anything waiting on first append (and/or 24504 // timestamp offset(s)) can process immediately. While the extra state isn't optimal, 24505 // we need some notion of whether the timestamp offset or other relevant information 24506 // has had a chance to be set. 24507 24508 24509 segmentInfo.hasAppendedData_ = true; // Now that the timestamp offset should be set, we can append any waiting ID3 tags. 24510 24511 this.processMetadataQueue_(); 24512 this.appendData_(segmentInfo, result); 24513 } 24514 24515 updateAppendInitSegmentStatus(segmentInfo, type) { 24516 // alt audio doesn't manage timestamp offset 24517 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 24518 // segment for each chunk 24519 !segmentInfo.changedTimestampOffset) { 24520 // if the timestamp offset changed, the timeline may have changed, so we have to re- 24521 // append init segments 24522 this.appendInitSegment_ = { 24523 audio: true, 24524 video: true 24525 }; 24526 } 24527 24528 if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) { 24529 // make sure we append init segment on playlist changes, in case the media config 24530 // changed 24531 this.appendInitSegment_[type] = true; 24532 } 24533 } 24534 24535 getInitSegmentAndUpdateState_({ 24536 type, 24537 initSegment, 24538 map, 24539 playlist 24540 }) { 24541 // "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section 24542 // (Section 3) required to parse the applicable Media Segments. It applies to every 24543 // Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag 24544 // or until the end of the playlist." 24545 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5 24546 if (map) { 24547 const id = initSegmentId(map); 24548 24549 if (this.activeInitSegmentId_ === id) { 24550 // don't need to re-append the init segment if the ID matches 24551 return null; 24552 } // a map-specified init segment takes priority over any transmuxed (or otherwise 24553 // obtained) init segment 24554 // 24555 // this also caches the init segment for later use 24556 24557 24558 initSegment = this.initSegmentForMap(map, true).bytes; 24559 this.activeInitSegmentId_ = id; 24560 } // We used to always prepend init segments for video, however, that shouldn't be 24561 // necessary. Instead, we should only append on changes, similar to what we've always 24562 // done for audio. This is more important (though may not be that important) for 24563 // frame-by-frame appending for LHLS, simply because of the increased quantity of 24564 // appends. 24565 24566 24567 if (initSegment && this.appendInitSegment_[type]) { 24568 // Make sure we track the playlist that we last used for the init segment, so that 24569 // we can re-append the init segment in the event that we get data from a new 24570 // playlist. Discontinuities and track changes are handled in other sections. 24571 this.playlistOfLastInitSegment_[type] = playlist; // Disable future init segment appends for this type. Until a change is necessary. 24572 24573 this.appendInitSegment_[type] = false; // we need to clear out the fmp4 active init segment id, since 24574 // we are appending the muxer init segment 24575 24576 this.activeInitSegmentId_ = null; 24577 return initSegment; 24578 } 24579 24580 return null; 24581 } 24582 24583 handleQuotaExceededError_({ 24584 segmentInfo, 24585 type, 24586 bytes 24587 }, error) { 24588 const audioBuffered = this.sourceUpdater_.audioBuffered(); 24589 const videoBuffered = this.sourceUpdater_.videoBuffered(); // For now we're ignoring any notion of gaps in the buffer, but they, in theory, 24590 // should be cleared out during the buffer removals. However, log in case it helps 24591 // debug. 24592 24593 if (audioBuffered.length > 1) { 24594 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' + timeRangesToArray(audioBuffered).join(', ')); 24595 } 24596 24597 if (videoBuffered.length > 1) { 24598 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' + timeRangesToArray(videoBuffered).join(', ')); 24599 } 24600 24601 const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0; 24602 const audioBufferEnd = audioBuffered.length ? audioBuffered.end(audioBuffered.length - 1) : 0; 24603 const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0; 24604 const videoBufferEnd = videoBuffered.length ? videoBuffered.end(videoBuffered.length - 1) : 0; 24605 24606 if (audioBufferEnd - audioBufferStart <= MIN_BACK_BUFFER && videoBufferEnd - videoBufferStart <= MIN_BACK_BUFFER) { 24607 // Can't remove enough buffer to make room for new segment (or the browser doesn't 24608 // allow for appends of segments this size). In the future, it may be possible to 24609 // split up the segment and append in pieces, but for now, error out this playlist 24610 // in an attempt to switch to a more manageable rendition. 24611 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(', ')}, `); 24612 this.error({ 24613 message: 'Quota exceeded error with append of a single segment of content', 24614 excludeUntil: Infinity 24615 }); 24616 this.trigger('error'); 24617 return; 24618 } // To try to resolve the quota exceeded error, clear back buffer and retry. This means 24619 // that the segment-loader should block on future events until this one is handled, so 24620 // that it doesn't keep moving onto further segments. Adding the call to the call 24621 // queue will prevent further appends until waitingOnRemove_ and 24622 // quotaExceededErrorRetryTimeout_ are cleared. 24623 // 24624 // Note that this will only block the current loader. In the case of demuxed content, 24625 // the other load may keep filling as fast as possible. In practice, this should be 24626 // OK, as it is a rare case when either audio has a high enough bitrate to fill up a 24627 // source buffer, or video fills without enough room for audio to append (and without 24628 // the availability of clearing out seconds of back buffer to make room for audio). 24629 // But it might still be good to handle this case in the future as a TODO. 24630 24631 24632 this.waitingOnRemove_ = true; 24633 this.callQueue_.push(this.appendToSourceBuffer_.bind(this, { 24634 segmentInfo, 24635 type, 24636 bytes 24637 })); 24638 const currentTime = this.currentTime_(); // Try to remove as much audio and video as possible to make room for new content 24639 // before retrying. 24640 24641 const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER; 24642 this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`); 24643 this.remove(0, timeToRemoveUntil, () => { 24644 this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`); 24645 this.waitingOnRemove_ = false; // wait the length of time alotted in the back buffer to prevent wasted 24646 // attempts (since we can't clear less than the minimum) 24647 24648 this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => { 24649 this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue'); 24650 this.quotaExceededErrorRetryTimeout_ = null; 24651 this.processCallQueue_(); 24652 }, MIN_BACK_BUFFER * 1000); 24653 }, true); 24654 } 24655 24656 handleAppendError_({ 24657 segmentInfo, 24658 type, 24659 bytes 24660 }, error) { 24661 // if there's no error, nothing to do 24662 if (!error) { 24663 return; 24664 } 24665 24666 if (error.code === QUOTA_EXCEEDED_ERR) { 24667 this.handleQuotaExceededError_({ 24668 segmentInfo, 24669 type, 24670 bytes 24671 }); // A quota exceeded error should be recoverable with a future re-append, so no need 24672 // to trigger an append error. 24673 24674 return; 24675 } 24676 24677 this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error); 24678 this.error(`${type} append of ${bytes.length}b failed for segment ` + `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`); // If an append errors, we often can't recover. 24679 // (see https://w3c.github.io/media-source/#sourcebuffer-append-error). 24680 // 24681 // Trigger a special error so that it can be handled separately from normal, 24682 // recoverable errors. 24683 24684 this.trigger('appenderror'); 24685 } 24686 24687 appendToSourceBuffer_({ 24688 segmentInfo, 24689 type, 24690 initSegment, 24691 data, 24692 bytes 24693 }) { 24694 // If this is a re-append, bytes were already created and don't need to be recreated 24695 if (!bytes) { 24696 const segments = [data]; 24697 let byteLength = data.byteLength; 24698 24699 if (initSegment) { 24700 // if the media initialization segment is changing, append it before the content 24701 // segment 24702 segments.unshift(initSegment); 24703 byteLength += initSegment.byteLength; 24704 } // Technically we should be OK appending the init segment separately, however, we 24705 // haven't yet tested that, and prepending is how we have always done things. 24706 24707 24708 bytes = concatSegments({ 24709 bytes: byteLength, 24710 segments 24711 }); 24712 } 24713 24714 this.sourceUpdater_.appendBuffer({ 24715 segmentInfo, 24716 type, 24717 bytes 24718 }, this.handleAppendError_.bind(this, { 24719 segmentInfo, 24720 type, 24721 bytes 24722 })); 24723 } 24724 24725 handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) { 24726 if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) { 24727 return; 24728 } 24729 24730 const segment = this.pendingSegment_.segment; 24731 const timingInfoProperty = `${type}TimingInfo`; 24732 24733 if (!segment[timingInfoProperty]) { 24734 segment[timingInfoProperty] = {}; 24735 } 24736 24737 segment[timingInfoProperty].transmuxerPrependedSeconds = segmentTimingInfo.prependedContentDuration || 0; 24738 segment[timingInfoProperty].transmuxedPresentationStart = segmentTimingInfo.start.presentation; 24739 segment[timingInfoProperty].transmuxedDecodeStart = segmentTimingInfo.start.decode; 24740 segment[timingInfoProperty].transmuxedPresentationEnd = segmentTimingInfo.end.presentation; 24741 segment[timingInfoProperty].transmuxedDecodeEnd = segmentTimingInfo.end.decode; // mainly used as a reference for debugging 24742 24743 segment[timingInfoProperty].baseMediaDecodeTime = segmentTimingInfo.baseMediaDecodeTime; 24744 } 24745 24746 appendData_(segmentInfo, result) { 24747 const { 24748 type, 24749 data 24750 } = result; 24751 24752 if (!data || !data.byteLength) { 24753 return; 24754 } 24755 24756 if (type === 'audio' && this.audioDisabled_) { 24757 return; 24758 } 24759 24760 const initSegment = this.getInitSegmentAndUpdateState_({ 24761 type, 24762 initSegment: result.initSegment, 24763 playlist: segmentInfo.playlist, 24764 map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null 24765 }); 24766 this.appendToSourceBuffer_({ 24767 segmentInfo, 24768 type, 24769 initSegment, 24770 data 24771 }); 24772 } 24773 /** 24774 * load a specific segment from a request into the buffer 24775 * 24776 * @private 24777 */ 24778 24779 24780 loadSegment_(segmentInfo) { 24781 this.state = 'WAITING'; 24782 this.pendingSegment_ = segmentInfo; 24783 this.trimBackBuffer_(segmentInfo); 24784 24785 if (typeof segmentInfo.timestampOffset === 'number') { 24786 if (this.transmuxer_) { 24787 this.transmuxer_.postMessage({ 24788 action: 'clearAllMp4Captions' 24789 }); 24790 } 24791 } 24792 24793 if (!this.hasEnoughInfoToLoad_()) { 24794 this.loadQueue_.push(() => { 24795 // regenerate the audioAppendStart, timestampOffset, etc as they 24796 // may have changed since this function was added to the queue. 24797 const options = _extends({}, segmentInfo, { 24798 forceTimestampOffset: true 24799 }); 24800 24801 _extends(segmentInfo, this.generateSegmentInfo_(options)); 24802 24803 this.isPendingTimestampOffset_ = false; 24804 this.updateTransmuxerAndRequestSegment_(segmentInfo); 24805 }); 24806 return; 24807 } 24808 24809 this.updateTransmuxerAndRequestSegment_(segmentInfo); 24810 } 24811 24812 updateTransmuxerAndRequestSegment_(segmentInfo) { 24813 // We'll update the source buffer's timestamp offset once we have transmuxed data, but 24814 // the transmuxer still needs to be updated before then. 24815 // 24816 // Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp 24817 // offset must be passed to the transmuxer for stream correcting adjustments. 24818 if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) { 24819 this.gopBuffer_.length = 0; // gopsToAlignWith was set before the GOP buffer was cleared 24820 24821 segmentInfo.gopsToAlignWith = []; 24822 this.timeMapping_ = 0; // reset values in the transmuxer since a discontinuity should start fresh 24823 24824 this.transmuxer_.postMessage({ 24825 action: 'reset' 24826 }); 24827 this.transmuxer_.postMessage({ 24828 action: 'setTimestampOffset', 24829 timestampOffset: segmentInfo.timestampOffset 24830 }); 24831 } 24832 24833 const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo); 24834 const isEndOfStream = this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex); 24835 const isWalkingForward = this.mediaIndex !== null; 24836 const isDiscontinuity = segmentInfo.timeline !== this.currentTimeline_ && // currentTimeline starts at -1, so we shouldn't end the timeline switching to 0, 24837 // the first timeline 24838 segmentInfo.timeline > 0; 24839 const isEndOfTimeline = isEndOfStream || isWalkingForward && isDiscontinuity; 24840 this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes), 24841 // then this init segment has never been seen before and should be appended. 24842 // 24843 // At this point the content type (audio/video or both) is not yet known, but it should be safe to set 24844 // both to true and leave the decision of whether to append the init segment to append time. 24845 24846 if (simpleSegment.map && !simpleSegment.map.bytes) { 24847 this.logger_('going to request init segment.'); 24848 this.appendInitSegment_ = { 24849 video: true, 24850 audio: true 24851 }; 24852 } 24853 24854 segmentInfo.abortRequests = mediaSegmentRequest({ 24855 xhr: this.vhs_.xhr, 24856 xhrOptions: this.xhrOptions_, 24857 decryptionWorker: this.decrypter_, 24858 segment: simpleSegment, 24859 abortFn: this.handleAbort_.bind(this, segmentInfo), 24860 progressFn: this.handleProgress_.bind(this), 24861 trackInfoFn: this.handleTrackInfo_.bind(this), 24862 timingInfoFn: this.handleTimingInfo_.bind(this), 24863 videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'video', segmentInfo.requestId), 24864 audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'audio', segmentInfo.requestId), 24865 captionsFn: this.handleCaptions_.bind(this), 24866 isEndOfTimeline, 24867 endedTimelineFn: () => { 24868 this.logger_('received endedtimeline callback'); 24869 }, 24870 id3Fn: this.handleId3_.bind(this), 24871 dataFn: this.handleData_.bind(this), 24872 doneFn: this.segmentRequestFinished_.bind(this), 24873 onTransmuxerLog: ({ 24874 message, 24875 level, 24876 stream 24877 }) => { 24878 this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); 24879 } 24880 }); 24881 } 24882 /** 24883 * trim the back buffer so that we don't have too much data 24884 * in the source buffer 24885 * 24886 * @private 24887 * 24888 * @param {Object} segmentInfo - the current segment 24889 */ 24890 24891 24892 trimBackBuffer_(segmentInfo) { 24893 const removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); // Chrome has a hard limit of 150MB of 24894 // buffer and a very conservative "garbage collector" 24895 // We manually clear out the old buffer to ensure 24896 // we don't trigger the QuotaExceeded error 24897 // on the source buffer during subsequent appends 24898 24899 if (removeToTime > 0) { 24900 this.remove(0, removeToTime); 24901 } 24902 } 24903 /** 24904 * created a simplified copy of the segment object with just the 24905 * information necessary to perform the XHR and decryption 24906 * 24907 * @private 24908 * 24909 * @param {Object} segmentInfo - the current segment 24910 * @return {Object} a simplified segment object copy 24911 */ 24912 24913 24914 createSimplifiedSegmentObj_(segmentInfo) { 24915 const segment = segmentInfo.segment; 24916 const part = segmentInfo.part; 24917 const simpleSegment = { 24918 resolvedUri: part ? part.resolvedUri : segment.resolvedUri, 24919 byterange: part ? part.byterange : segment.byterange, 24920 requestId: segmentInfo.requestId, 24921 transmuxer: segmentInfo.transmuxer, 24922 audioAppendStart: segmentInfo.audioAppendStart, 24923 gopsToAlignWith: segmentInfo.gopsToAlignWith, 24924 part: segmentInfo.part 24925 }; 24926 const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1]; 24927 24928 if (previousSegment && previousSegment.timeline === segment.timeline) { 24929 // The baseStartTime of a segment is used to handle rollover when probing the TS 24930 // segment to retrieve timing information. Since the probe only looks at the media's 24931 // times (e.g., PTS and DTS values of the segment), and doesn't consider the 24932 // player's time (e.g., player.currentTime()), baseStartTime should reflect the 24933 // media time as well. transmuxedDecodeEnd represents the end time of a segment, in 24934 // seconds of media time, so should be used here. The previous segment is used since 24935 // the end of the previous segment should represent the beginning of the current 24936 // segment, so long as they are on the same timeline. 24937 if (previousSegment.videoTimingInfo) { 24938 simpleSegment.baseStartTime = previousSegment.videoTimingInfo.transmuxedDecodeEnd; 24939 } else if (previousSegment.audioTimingInfo) { 24940 simpleSegment.baseStartTime = previousSegment.audioTimingInfo.transmuxedDecodeEnd; 24941 } 24942 } 24943 24944 if (segment.key) { 24945 // if the media sequence is greater than 2^32, the IV will be incorrect 24946 // assuming 10s segments, that would be about 1300 years 24947 const iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 24948 simpleSegment.key = this.segmentKey(segment.key); 24949 simpleSegment.key.iv = iv; 24950 } 24951 24952 if (segment.map) { 24953 simpleSegment.map = this.initSegmentForMap(segment.map); 24954 } 24955 24956 return simpleSegment; 24957 } 24958 24959 saveTransferStats_(stats) { 24960 // every request counts as a media request even if it has been aborted 24961 // or canceled due to a timeout 24962 this.mediaRequests += 1; 24963 24964 if (stats) { 24965 this.mediaBytesTransferred += stats.bytesReceived; 24966 this.mediaTransferDuration += stats.roundTripTime; 24967 } 24968 } 24969 24970 saveBandwidthRelatedStats_(duration, stats) { 24971 // byteLength will be used for throughput, and should be based on bytes receieved, 24972 // which we only know at the end of the request and should reflect total bytes 24973 // downloaded rather than just bytes processed from components of the segment 24974 this.pendingSegment_.byteLength = stats.bytesReceived; 24975 24976 if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 24977 this.logger_(`Ignoring segment's bandwidth because its duration of ${duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`); 24978 return; 24979 }
vendor: 12,410 bytes, lines 24980-25325
24980 24981 this.bandwidth = stats.bandwidth; 24982 this.roundTrip = stats.roundTripTime; 24983 } 24984 24985 handleTimeout_() { 24986 // although the VTT segment loader bandwidth isn't really used, it's good to 24987 // maintain functinality between segment loaders 24988 this.mediaRequestsTimedout += 1; 24989 this.bandwidth = 1; 24990 this.roundTrip = NaN; 24991 this.trigger('bandwidthupdate'); 24992 this.trigger('timeout'); 24993 } 24994 /** 24995 * Handle the callback from the segmentRequest function and set the 24996 * associated SegmentLoader state and errors if necessary 24997 * 24998 * @private 24999 */ 25000 25001 25002 segmentRequestFinished_(error, simpleSegment, result) { 25003 // TODO handle special cases, e.g., muxed audio/video but only audio in the segment 25004 // check the call queue directly since this function doesn't need to deal with any 25005 // data, and can continue even if the source buffers are not set up and we didn't get 25006 // any data from the segment 25007 if (this.callQueue_.length) { 25008 this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result)); 25009 return; 25010 } 25011 25012 this.saveTransferStats_(simpleSegment.stats); // The request was aborted and the SegmentLoader has already been reset 25013 25014 if (!this.pendingSegment_) { 25015 return; 25016 } // the request was aborted and the SegmentLoader has already started 25017 // another request. this can happen when the timeout for an aborted 25018 // request triggers due to a limitation in the XHR library 25019 // do not count this as any sort of request or we risk double-counting 25020 25021 25022 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 25023 return; 25024 } // an error occurred from the active pendingSegment_ so reset everything 25025 25026 25027 if (error) { 25028 this.pendingSegment_ = null; 25029 this.state = 'READY'; // aborts are not a true error condition and nothing corrective needs to be done 25030 25031 if (error.code === REQUEST_ERRORS.ABORTED) { 25032 return; 25033 } 25034 25035 this.pause(); // the error is really just that at least one of the requests timed-out 25036 // set the bandwidth to a very low value and trigger an ABR switch to 25037 // take emergency action 25038 25039 if (error.code === REQUEST_ERRORS.TIMEOUT) { 25040 this.handleTimeout_(); 25041 return; 25042 } // if control-flow has arrived here, then the error is real 25043 // emit an error event to exclude the current playlist 25044 25045 25046 this.mediaRequestsErrored += 1; 25047 this.error(error); 25048 this.trigger('error'); 25049 return; 25050 } 25051 25052 const segmentInfo = this.pendingSegment_; // the response was a success so set any bandwidth stats the request 25053 // generated for ABR purposes 25054 25055 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); 25056 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; 25057 25058 if (result.gopInfo) { 25059 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_); 25060 } // Although we may have already started appending on progress, we shouldn't switch the 25061 // state away from loading until we are officially done loading the segment data. 25062 25063 25064 this.state = 'APPENDING'; // used for testing 25065 25066 this.trigger('appending'); 25067 this.waitForAppendsToComplete_(segmentInfo); 25068 } 25069 25070 setTimeMapping_(timeline) { 25071 const timelineMapping = this.syncController_.mappingForTimeline(timeline); 25072 25073 if (timelineMapping !== null) { 25074 this.timeMapping_ = timelineMapping; 25075 } 25076 } 25077 25078 updateMediaSecondsLoaded_(segment) { 25079 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 25080 this.mediaSecondsLoaded += segment.end - segment.start; 25081 } else { 25082 this.mediaSecondsLoaded += segment.duration; 25083 } 25084 } 25085 25086 shouldUpdateTransmuxerTimestampOffset_(timestampOffset) { 25087 if (timestampOffset === null) { 25088 return false; 25089 } // note that we're potentially using the same timestamp offset for both video and 25090 // audio 25091 25092 25093 if (this.loaderType_ === 'main' && timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 25094 return true; 25095 } 25096 25097 if (!this.audioDisabled_ && timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 25098 return true; 25099 } 25100 25101 return false; 25102 } 25103 25104 trueSegmentStart_({ 25105 currentStart, 25106 playlist, 25107 mediaIndex, 25108 firstVideoFrameTimeForData, 25109 currentVideoTimestampOffset, 25110 useVideoTimingInfo, 25111 videoTimingInfo, 25112 audioTimingInfo 25113 }) { 25114 if (typeof currentStart !== 'undefined') { 25115 // if start was set once, keep using it 25116 return currentStart; 25117 } 25118 25119 if (!useVideoTimingInfo) { 25120 return audioTimingInfo.start; 25121 } 25122 25123 const previousSegment = playlist.segments[mediaIndex - 1]; // The start of a segment should be the start of the first full frame contained 25124 // within that segment. Since the transmuxer maintains a cache of incomplete data 25125 // from and/or the last frame seen, the start time may reflect a frame that starts 25126 // in the previous segment. Check for that case and ensure the start time is 25127 // accurate for the segment. 25128 25129 if (mediaIndex === 0 || !previousSegment || typeof previousSegment.start === 'undefined' || previousSegment.end !== firstVideoFrameTimeForData + currentVideoTimestampOffset) { 25130 return firstVideoFrameTimeForData; 25131 } 25132 25133 return videoTimingInfo.start; 25134 } 25135 25136 waitForAppendsToComplete_(segmentInfo) { 25137 const trackInfo = this.getCurrentMediaInfo_(segmentInfo); 25138 25139 if (!trackInfo) { 25140 this.error({ 25141 message: 'No starting media returned, likely due to an unsupported media format.', 25142 playlistExclusionDuration: Infinity 25143 }); 25144 this.trigger('error'); 25145 return; 25146 } // Although transmuxing is done, appends may not yet be finished. Throw a marker 25147 // on each queue this loader is responsible for to ensure that the appends are 25148 // complete. 25149 25150 25151 const { 25152 hasAudio, 25153 hasVideo, 25154 isMuxed 25155 } = trackInfo; 25156 const waitForVideo = this.loaderType_ === 'main' && hasVideo; 25157 const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed; 25158 segmentInfo.waitingOnAppends = 0; // segments with no data 25159 25160 if (!segmentInfo.hasAppendedData_) { 25161 if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') { 25162 // When there's no audio or video data in the segment, there's no audio or video 25163 // timing information. 25164 // 25165 // If there's no audio or video timing information, then the timestamp offset 25166 // can't be adjusted to the appropriate value for the transmuxer and source 25167 // buffers. 25168 // 25169 // Therefore, the next segment should be used to set the timestamp offset. 25170 this.isPendingTimestampOffset_ = true; 25171 } // override settings for metadata only segments 25172 25173 25174 segmentInfo.timingInfo = { 25175 start: 0 25176 }; 25177 segmentInfo.waitingOnAppends++; 25178 25179 if (!this.isPendingTimestampOffset_) { 25180 // update the timestampoffset 25181 this.updateSourceBufferTimestampOffset_(segmentInfo); // make sure the metadata queue is processed even though we have 25182 // no video/audio data. 25183 25184 this.processMetadataQueue_(); 25185 } // append is "done" instantly with no data. 25186 25187 25188 this.checkAppendsDone_(segmentInfo); 25189 return; 25190 } // Since source updater could call back synchronously, do the increments first. 25191 25192 25193 if (waitForVideo) { 25194 segmentInfo.waitingOnAppends++; 25195 } 25196 25197 if (waitForAudio) { 25198 segmentInfo.waitingOnAppends++; 25199 } 25200 25201 if (waitForVideo) { 25202 this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 25203 } 25204 25205 if (waitForAudio) { 25206 this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 25207 } 25208 } 25209 25210 checkAppendsDone_(segmentInfo) { 25211 if (this.checkForAbort_(segmentInfo.requestId)) { 25212 return; 25213 } 25214 25215 segmentInfo.waitingOnAppends--; 25216 25217 if (segmentInfo.waitingOnAppends === 0) { 25218 this.handleAppendsDone_(); 25219 } 25220 } 25221 25222 checkForIllegalMediaSwitch(trackInfo) { 25223 const illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.getCurrentMediaInfo_(), trackInfo); 25224 25225 if (illegalMediaSwitchError) { 25226 this.error({ 25227 message: illegalMediaSwitchError, 25228 playlistExclusionDuration: Infinity 25229 }); 25230 this.trigger('error'); 25231 return true; 25232 } 25233 25234 return false; 25235 } 25236 25237 updateSourceBufferTimestampOffset_(segmentInfo) { 25238 if (segmentInfo.timestampOffset === null || // we don't yet have the start for whatever media type (video or audio) has 25239 // priority, timing-wise, so we must wait 25240 typeof segmentInfo.timingInfo.start !== 'number' || // already updated the timestamp offset for this segment 25241 segmentInfo.changedTimestampOffset || // the alt audio loader should not be responsible for setting the timestamp offset 25242 this.loaderType_ !== 'main') { 25243 return; 25244 } 25245 25246 let didChange = false; // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that 25247 // the timing info here comes from video. In the event that the audio is longer than 25248 // the video, this will trim the start of the audio. 25249 // This also trims any offset from 0 at the beginning of the media 25250 25251 segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({ 25252 videoTimingInfo: segmentInfo.segment.videoTimingInfo, 25253 audioTimingInfo: segmentInfo.segment.audioTimingInfo, 25254 timingInfo: segmentInfo.timingInfo 25255 }); // In the event that there are part segment downloads, each will try to update the 25256 // timestamp offset. Retaining this bit of state prevents us from updating in the 25257 // future (within the same segment), however, there may be a better way to handle it. 25258 25259 segmentInfo.changedTimestampOffset = true; 25260 25261 if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 25262 this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset); 25263 didChange = true; 25264 } 25265 25266 if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 25267 this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset); 25268 didChange = true; 25269 } 25270 25271 if (didChange) { 25272 this.trigger('timestampoffset'); 25273 } 25274 } 25275 25276 getSegmentStartTimeForTimestampOffsetCalculation_({ 25277 videoTimingInfo, 25278 audioTimingInfo, 25279 timingInfo 25280 }) { 25281 if (!this.useDtsForTimestampOffset_) { 25282 return timingInfo.start; 25283 } 25284 25285 if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') { 25286 return videoTimingInfo.transmuxedDecodeStart; 25287 } // handle audio only 25288 25289 25290 if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') { 25291 return audioTimingInfo.transmuxedDecodeStart; 25292 } // handle content not transmuxed (e.g., MP4) 25293 25294 25295 return timingInfo.start; 25296 } 25297 25298 updateTimingInfoEnd_(segmentInfo) { 25299 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 25300 const trackInfo = this.getMediaInfo_(); 25301 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 25302 const prioritizedTimingInfo = useVideoTimingInfo && segmentInfo.videoTimingInfo ? segmentInfo.videoTimingInfo : segmentInfo.audioTimingInfo; 25303 25304 if (!prioritizedTimingInfo) { 25305 return; 25306 } 25307 25308 segmentInfo.timingInfo.end = typeof prioritizedTimingInfo.end === 'number' ? // End time may not exist in a case where we aren't parsing the full segment (one 25309 // current example is the case of fmp4), so use the rough duration to calculate an 25310 // end time. 25311 prioritizedTimingInfo.end : prioritizedTimingInfo.start + segmentInfo.duration; 25312 } 25313 /** 25314 * callback to run when appendBuffer is finished. detects if we are 25315 * in a good state to do things with the data we got, or if we need 25316 * to wait for more 25317 * 25318 * @private 25319 */ 25320 25321 25322 handleAppendsDone_() { 25323 // appendsdone can cause an abort 25324 if (this.pendingSegment_) { 25325
25325this.trigger('appendsdone'); 25326 } 25327 25328 if (!this.pendingSegment_) { 25329 this.state = 'READY'; // TODO should this move into this.checkForAbort to speed up requests post abort in 25330 // all appending cases? 25331 25332 if (!this.paused()) { 25333 this.monitorBuffer_(); 25334 } 25335 25336 return; 25337 } 25338 25339 const segmentInfo = this.pendingSegment_; // Now that the end of the segment has been reached, we can set the end time. It's 25340 // best to wait until all appends are done so we're sure that the primary media is 25341 // finished (and we have its end time).
vendor: 18,125 bytes, lines 25341-25799
25341 25342 25343 this.updateTimingInfoEnd_(segmentInfo); 25344 25345 if (this.shouldSaveSegmentTimingInfo_) { 25346 // Timeline mappings should only be saved for the main loader. This is for multiple 25347 // reasons: 25348 // 25349 // 1) Only one mapping is saved per timeline, meaning that if both the audio loader 25350 // and the main loader try to save the timeline mapping, whichever comes later 25351 // will overwrite the first. In theory this is OK, as the mappings should be the 25352 // same, however, it breaks for (2) 25353 // 2) In the event of a live stream, the initial live point will make for a somewhat 25354 // arbitrary mapping. If audio and video streams are not perfectly in-sync, then 25355 // the mapping will be off for one of the streams, dependent on which one was 25356 // first saved (see (1)). 25357 // 3) Primary timing goes by video in VHS, so the mapping should be video. 25358 // 25359 // Since the audio loader will wait for the main loader to load the first segment, 25360 // the main loader will save the first timeline mapping, and ensure that there won't 25361 // be a case where audio loads two segments without saving a mapping (thus leading 25362 // to missing segment timing info). 25363 this.syncController_.saveSegmentTimingInfo({ 25364 segmentInfo, 25365 shouldSaveTimelineMapping: this.loaderType_ === 'main' 25366 }); 25367 } 25368 25369 const segmentDurationMessage = getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_); 25370 25371 if (segmentDurationMessage) { 25372 if (segmentDurationMessage.severity === 'warn') { 25373 videojs__default["default"].log.warn(segmentDurationMessage.message); 25374 } else { 25375 this.logger_(segmentDurationMessage.message); 25376 } 25377 } 25378 25379 this.recordThroughput_(segmentInfo); 25380 this.pendingSegment_ = null; 25381 this.state = 'READY'; 25382 25383 if (segmentInfo.isSyncRequest) { 25384 this.trigger('syncinfoupdate'); // if the sync request was not appended 25385 // then it was not the correct segment. 25386 // throw it away and use the data it gave us 25387 // to get the correct one. 25388 25389 if (!segmentInfo.hasAppendedData_) { 25390 this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`); 25391 return; 25392 } 25393 } 25394 25395 this.logger_(`Appended ${segmentInfoString(segmentInfo)}`); 25396 this.addSegmentMetadataCue_(segmentInfo); 25397 this.fetchAtBuffer_ = true; 25398 25399 if (this.currentTimeline_ !== segmentInfo.timeline) { 25400 this.timelineChangeController_.lastTimelineChange({ 25401 type: this.loaderType_, 25402 from: this.currentTimeline_, 25403 to: segmentInfo.timeline 25404 }); // If audio is not disabled, the main segment loader is responsible for updating 25405 // the audio timeline as well. If the content is video only, this won't have any 25406 // impact. 25407 25408 if (this.loaderType_ === 'main' && !this.audioDisabled_) { 25409 this.timelineChangeController_.lastTimelineChange({ 25410 type: 'audio', 25411 from: this.currentTimeline_, 25412 to: segmentInfo.timeline 25413 }); 25414 } 25415 } 25416 25417 this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before 25418 // the following conditional otherwise it may consider this a bad "guess" 25419 // and attempt to resync when the post-update seekable window and live 25420 // point would mean that this was the perfect segment to fetch 25421 25422 this.trigger('syncinfoupdate'); 25423 const segment = segmentInfo.segment; 25424 const part = segmentInfo.part; 25425 const badSegmentGuess = segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3; 25426 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 25427 // the currentTime_ that means that our conservative guess was too conservative. 25428 // In that case, reset the loader state so that we try to use any information gained 25429 // from the previous request to create a new, more accurate, sync-point. 25430 25431 if (badSegmentGuess || badPartGuess) { 25432 this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`); 25433 this.resetEverything(); 25434 return; 25435 } 25436 25437 const isWalkingForward = this.mediaIndex !== null; // Don't do a rendition switch unless we have enough time to get a sync segment 25438 // and conservatively guess 25439 25440 if (isWalkingForward) { 25441 this.trigger('bandwidthupdate'); 25442 } 25443 25444 this.trigger('progress'); 25445 this.mediaIndex = segmentInfo.mediaIndex; 25446 this.partIndex = segmentInfo.partIndex; // any time an update finishes and the last segment is in the 25447 // buffer, end the stream. this ensures the "ended" event will 25448 // fire if playback reaches that point. 25449 25450 if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) { 25451 this.endOfStream(); 25452 } // used for testing 25453 25454 25455 this.trigger('appended'); 25456 25457 if (segmentInfo.hasAppendedData_) { 25458 this.mediaAppends++; 25459 } 25460 25461 if (!this.paused()) { 25462 this.monitorBuffer_(); 25463 } 25464 } 25465 /** 25466 * Records the current throughput of the decrypt, transmux, and append 25467 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 25468 * moving average of the throughput. `throughput.count` is the number of 25469 * data points in the average. 25470 * 25471 * @private 25472 * @param {Object} segmentInfo the object returned by loadSegment 25473 */ 25474 25475 25476 recordThroughput_(segmentInfo) { 25477 if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 25478 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}`); 25479 return; 25480 } 25481 25482 const rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide 25483 // by zero in the case where the throughput is ridiculously high 25484 25485 const segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second 25486 25487 const segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); // This is just a cumulative moving average calculation: 25488 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 25489 25490 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 25491 } 25492 /** 25493 * Adds a cue to the segment-metadata track with some metadata information about the 25494 * segment 25495 * 25496 * @private 25497 * @param {Object} segmentInfo 25498 * the object returned by loadSegment 25499 * @method addSegmentMetadataCue_ 25500 */ 25501 25502 25503 addSegmentMetadataCue_(segmentInfo) { 25504 if (!this.segmentMetadataTrack_) { 25505 return; 25506 } 25507 25508 const segment = segmentInfo.segment; 25509 const start = segment.start; 25510 const end = segment.end; // Do not try adding the cue if the start and end times are invalid. 25511 25512 if (!finite(start) || !finite(end)) { 25513 return; 25514 } 25515 25516 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 25517 const Cue = window.WebKitDataCue || window.VTTCue; 25518 const value = { 25519 custom: segment.custom, 25520 dateTimeObject: segment.dateTimeObject, 25521 dateTimeString: segment.dateTimeString, 25522 programDateTime: segment.programDateTime, 25523 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 25524 resolution: segmentInfo.playlist.attributes.RESOLUTION, 25525 codecs: segmentInfo.playlist.attributes.CODECS, 25526 byteLength: segmentInfo.byteLength, 25527 uri: segmentInfo.uri, 25528 timeline: segmentInfo.timeline, 25529 playlist: segmentInfo.playlist.id, 25530 start, 25531 end 25532 }; 25533 const data = JSON.stringify(value); 25534 const cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between 25535 // the differences of WebKitDataCue in safari and VTTCue in other browsers 25536 25537 cue.value = value; 25538 this.segmentMetadataTrack_.addCue(cue); 25539 } 25540 25541 } 25542 25543 function noop() {} 25544 25545 const toTitleCase = function (string) { 25546 if (typeof string !== 'string') { 25547 return string; 25548 } 25549 25550 return string.replace(/./, w => w.toUpperCase()); 25551 }; 25552 25553 /** 25554 * @file source-updater.js 25555 */ 25556 const bufferTypes = ['video', 'audio']; 25557 25558 const updating = (type, sourceUpdater) => { 25559 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 25560 return sourceBuffer && sourceBuffer.updating || sourceUpdater.queuePending[type]; 25561 }; 25562 25563 const nextQueueIndexOfType = (type, queue) => { 25564 for (let i = 0; i < queue.length; i++) { 25565 const queueEntry = queue[i]; 25566 25567 if (queueEntry.type === 'mediaSource') { 25568 // If the next entry is a media source entry (uses multiple source buffers), block 25569 // processing to allow it to go through first. 25570 return null; 25571 } 25572 25573 if (queueEntry.type === type) { 25574 return i; 25575 } 25576 } 25577 25578 return null; 25579 }; 25580 25581 const shiftQueue = (type, sourceUpdater) => { 25582 if (sourceUpdater.queue.length === 0) { 25583 return; 25584 } 25585 25586 let queueIndex = 0; 25587 let queueEntry = sourceUpdater.queue[queueIndex]; 25588 25589 if (queueEntry.type === 'mediaSource') { 25590 if (!sourceUpdater.updating() && sourceUpdater.mediaSource.readyState !== 'closed') { 25591 sourceUpdater.queue.shift(); 25592 queueEntry.action(sourceUpdater); 25593 25594 if (queueEntry.doneFn) { 25595 queueEntry.doneFn(); 25596 } // Only specific source buffer actions must wait for async updateend events. Media 25597 // Source actions process synchronously. Therefore, both audio and video source 25598 // buffers are now clear to process the next queue entries. 25599 25600 25601 shiftQueue('audio', sourceUpdater); 25602 shiftQueue('video', sourceUpdater); 25603 } // Media Source actions require both source buffers, so if the media source action 25604 // couldn't process yet (because one or both source buffers are busy), block other 25605 // queue actions until both are available and the media source action can process. 25606 25607 25608 return; 25609 } 25610 25611 if (type === 'mediaSource') { 25612 // If the queue was shifted by a media source action (this happens when pushing a 25613 // media source action onto the queue), then it wasn't from an updateend event from an 25614 // audio or video source buffer, so there's no change from previous state, and no 25615 // processing should be done. 25616 return; 25617 } // Media source queue entries don't need to consider whether the source updater is 25618 // started (i.e., source buffers are created) as they don't need the source buffers, but 25619 // source buffer queue entries do. 25620 25621 25622 if (!sourceUpdater.ready() || sourceUpdater.mediaSource.readyState === 'closed' || updating(type, sourceUpdater)) { 25623 return; 25624 } 25625 25626 if (queueEntry.type !== type) { 25627 queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue); 25628 25629 if (queueIndex === null) { 25630 // Either there's no queue entry that uses this source buffer type in the queue, or 25631 // there's a media source queue entry before the next entry of this type, in which 25632 // case wait for that action to process first. 25633 return; 25634 } 25635 25636 queueEntry = sourceUpdater.queue[queueIndex]; 25637 } 25638 25639 sourceUpdater.queue.splice(queueIndex, 1); // Keep a record that this source buffer type is in use. 25640 // 25641 // The queue pending operation must be set before the action is performed in the event 25642 // that the action results in a synchronous event that is acted upon. For instance, if 25643 // an exception is thrown that can be handled, it's possible that new actions will be 25644 // appended to an empty queue and immediately executed, but would not have the correct 25645 // pending information if this property was set after the action was performed. 25646 25647 sourceUpdater.queuePending[type] = queueEntry; 25648 queueEntry.action(type, sourceUpdater); 25649 25650 if (!queueEntry.doneFn) { 25651 // synchronous operation, process next entry 25652 sourceUpdater.queuePending[type] = null; 25653 shiftQueue(type, sourceUpdater); 25654 return; 25655 } 25656 }; 25657 25658 const cleanupBuffer = (type, sourceUpdater) => { 25659 const buffer = sourceUpdater[`${type}Buffer`]; 25660 const titleType = toTitleCase(type); 25661 25662 if (!buffer) { 25663 return; 25664 } 25665 25666 buffer.removeEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 25667 buffer.removeEventListener('error', sourceUpdater[`on${titleType}Error_`]); 25668 sourceUpdater.codecs[type] = null; 25669 sourceUpdater[`${type}Buffer`] = null; 25670 }; 25671 25672 const inSourceBuffers = (mediaSource, sourceBuffer) => mediaSource && sourceBuffer && Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1; 25673 25674 const actions = { 25675 appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => { 25676 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 25677 // or the media source does not contain this source buffer. 25678 25679 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 25680 return; 25681 } 25682 25683 sourceUpdater.logger_(`Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer`); 25684 25685 try { 25686 sourceBuffer.appendBuffer(bytes); 25687 } catch (e) { 25688 sourceUpdater.logger_(`Error with code ${e.code} ` + (e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') + `when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer`); 25689 sourceUpdater.queuePending[type] = null; 25690 onError(e); 25691 } 25692 }, 25693 remove: (start, end) => (type, sourceUpdater) => { 25694 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 25695 // or the media source does not contain this source buffer. 25696 25697 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 25698 return; 25699 } 25700 25701 sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`); 25702 25703 try { 25704 sourceBuffer.remove(start, end); 25705 } catch (e) { 25706 sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`); 25707 } 25708 }, 25709 timestampOffset: offset => (type, sourceUpdater) => { 25710 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 25711 // or the media source does not contain this source buffer. 25712 25713 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 25714 return; 25715 } 25716 25717 sourceUpdater.logger_(`Setting ${type}timestampOffset to ${offset}`); 25718 sourceBuffer.timestampOffset = offset; 25719 }, 25720 callback: callback => (type, sourceUpdater) => { 25721 callback(); 25722 }, 25723 endOfStream: error => sourceUpdater => { 25724 if (sourceUpdater.mediaSource.readyState !== 'open') { 25725 return; 25726 } 25727 25728 sourceUpdater.logger_(`Calling mediaSource endOfStream(${error || ''})`); 25729 25730 try { 25731 sourceUpdater.mediaSource.endOfStream(error); 25732 } catch (e) { 25733 videojs__default["default"].log.warn('Failed to call media source endOfStream', e); 25734 } 25735 }, 25736 duration: duration => sourceUpdater => { 25737 sourceUpdater.logger_(`Setting mediaSource duration to ${duration}`); 25738 25739 try { 25740 sourceUpdater.mediaSource.duration = duration; 25741 } catch (e) { 25742 videojs__default["default"].log.warn('Failed to set media source duration', e); 25743 } 25744 }, 25745 abort: () => (type, sourceUpdater) => { 25746 if (sourceUpdater.mediaSource.readyState !== 'open') { 25747 return; 25748 } 25749 25750 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 25751 // or the media source does not contain this source buffer. 25752 25753 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 25754 return; 25755 } 25756 25757 sourceUpdater.logger_(`calling abort on ${type}Buffer`); 25758 25759 try { 25760 sourceBuffer.abort(); 25761 } catch (e) { 25762 videojs__default["default"].log.warn(`Failed to abort on ${type}Buffer`, e); 25763 } 25764 }, 25765 addSourceBuffer: (type, codec) => sourceUpdater => { 25766 const titleType = toTitleCase(type); 25767 const mime = getMimeForCodec(codec); 25768 sourceUpdater.logger_(`Adding ${type}Buffer with codec ${codec} to mediaSource`); 25769 const sourceBuffer = sourceUpdater.mediaSource.addSourceBuffer(mime); 25770 sourceBuffer.addEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 25771 sourceBuffer.addEventListener('error', sourceUpdater[`on${titleType}Error_`]); 25772 sourceUpdater.codecs[type] = codec; 25773 sourceUpdater[`${type}Buffer`] = sourceBuffer; 25774 }, 25775 removeSourceBuffer: type => sourceUpdater => { 25776 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 25777 cleanupBuffer(type, sourceUpdater); // can't do anything if the media source / source buffer is null 25778 // or the media source does not contain this source buffer. 25779 25780 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 25781 return; 25782 } 25783 25784 sourceUpdater.logger_(`Removing ${type}Buffer with codec ${sourceUpdater.codecs[type]} from mediaSource`); 25785 25786 try { 25787 sourceUpdater.mediaSource.removeSourceBuffer(sourceBuffer); 25788 } catch (e) { 25789 videojs__default["default"].log.warn(`Failed to removeSourceBuffer ${type}Buffer`, e); 25790 } 25791 }, 25792 changeType: codec => (type, sourceUpdater) => { 25793 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 25794 const mime = getMimeForCodec(codec); // can't do anything if the media source / source buffer is null 25795 // or the media source does not contain this source buffer. 25796 25797 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 25798 return; 25799 } // do not update codec if we don't need to.
25800 25801 25802 if (sourceUpdater.codecs[type] === codec) { 25803 return; 25804 } 25805 25806 sourceUpdater.logger_(`changing ${type}Buffer codec from ${sourceUpdater.codecs[type]} to ${codec}`); // check if change to the provided type is supported 25807 25808 try { 25809 sourceBuffer.changeType(mime); 25810 sourceUpdater.codecs[type] = codec; 25811 } catch (e) { 25812 videojs__default["default"].log.warn(`Failed to changeType on ${type}Buffer`, e); 25813 } 25814 } 25815 }; 25816 25817 const pushQueue = ({ 25818 type, 25819 sourceUpdater, 25820 action, 25821 doneFn, 25822 name 25823 }) => { 25824 sourceUpdater.queue.push({ 25825 type, 25826 action, 25827 doneFn, 25828 name 25829 }); 25830 shiftQueue(type, sourceUpdater); 25831 }; 25832 25833 const onUpdateend = (type, sourceUpdater) => e => { 25834 // Although there should, in theory, be a pending action for any updateend receieved, 25835 // there are some actions that may trigger updateend events without set definitions in 25836 // the w3c spec. For instance, setting the duration on the media source may trigger 25837 // updateend events on source buffers. This does not appear to be in the spec. As such, 25838 // if we encounter an updateend without a corresponding pending action from our queue 25839 // for that source buffer type, process the next action. 25840 if (sourceUpdater.queuePending[type]) { 25841 const doneFn = sourceUpdater.queuePending[type].doneFn; 25842 sourceUpdater.queuePending[type] = null; 25843 25844 if (doneFn) { 25845 // if there's an error, report it 25846 doneFn(sourceUpdater[`${type}Error_`]); 25847 } 25848 } 25849 25850 shiftQueue(type, sourceUpdater); 25851 }; 25852 /** 25853 * A queue of callbacks to be serialized and applied when a
vendor: 6,791 bytes, lines 25854-26095
25854 * MediaSource and its associated SourceBuffers are not in the 25855 * updating state. It is used by the segment loader to update the 25856 * underlying SourceBuffers when new data is loaded, for instance. 25857 * 25858 * @class SourceUpdater 25859 * @param {MediaSource} mediaSource the MediaSource to create the SourceBuffer from 25860 * @param {string} mimeType the desired MIME type of the underlying SourceBuffer 25861 */ 25862 25863 25864 class SourceUpdater extends videojs__default["default"].EventTarget { 25865 constructor(mediaSource) { 25866 super(); 25867 this.mediaSource = mediaSource; 25868 25869 this.sourceopenListener_ = () => shiftQueue('mediaSource', this); 25870 25871 this.mediaSource.addEventListener('sourceopen', this.sourceopenListener_); 25872 this.logger_ = logger('SourceUpdater'); // initial timestamp offset is 0 25873 25874 this.audioTimestampOffset_ = 0; 25875 this.videoTimestampOffset_ = 0; 25876 this.queue = []; 25877 this.queuePending = { 25878 audio: null, 25879 video: null 25880 }; 25881 this.delayedAudioAppendQueue_ = []; 25882 this.videoAppendQueued_ = false; 25883 this.codecs = {}; 25884 this.onVideoUpdateEnd_ = onUpdateend('video', this); 25885 this.onAudioUpdateEnd_ = onUpdateend('audio', this); 25886 25887 this.onVideoError_ = e => { 25888 // used for debugging 25889 this.videoError_ = e; 25890 }; 25891 25892 this.onAudioError_ = e => { 25893 // used for debugging 25894 this.audioError_ = e; 25895 }; 25896 25897 this.createdSourceBuffers_ = false; 25898 this.initializedEme_ = false; 25899 this.triggeredReady_ = false; 25900 } 25901 25902 initializedEme() { 25903 this.initializedEme_ = true; 25904 this.triggerReady(); 25905 } 25906 25907 hasCreatedSourceBuffers() { 25908 // if false, likely waiting on one of the segment loaders to get enough data to create 25909 // source buffers 25910 return this.createdSourceBuffers_; 25911 } 25912 25913 hasInitializedAnyEme() { 25914 return this.initializedEme_; 25915 } 25916 25917 ready() { 25918 return this.hasCreatedSourceBuffers() && this.hasInitializedAnyEme(); 25919 } 25920 25921 createSourceBuffers(codecs) { 25922 if (this.hasCreatedSourceBuffers()) { 25923 // already created them before 25924 return; 25925 } // the intial addOrChangeSourceBuffers will always be 25926 // two add buffers. 25927 25928 25929 this.addOrChangeSourceBuffers(codecs); 25930 this.createdSourceBuffers_ = true; 25931 this.trigger('createdsourcebuffers'); 25932 this.triggerReady(); 25933 } 25934 25935 triggerReady() { 25936 // only allow ready to be triggered once, this prevents the case 25937 // where: 25938 // 1. we trigger createdsourcebuffers 25939 // 2. ie 11 synchronously initializates eme 25940 // 3. the synchronous initialization causes us to trigger ready 25941 // 4. We go back to the ready check in createSourceBuffers and ready is triggered again. 25942 if (this.ready() && !this.triggeredReady_) { 25943 this.triggeredReady_ = true; 25944 this.trigger('ready'); 25945 } 25946 } 25947 /** 25948 * Add a type of source buffer to the media source. 25949 * 25950 * @param {string} type 25951 * The type of source buffer to add. 25952 * 25953 * @param {string} codec 25954 * The codec to add the source buffer with. 25955 */ 25956 25957 25958 addSourceBuffer(type, codec) { 25959 pushQueue({ 25960 type: 'mediaSource', 25961 sourceUpdater: this, 25962 action: actions.addSourceBuffer(type, codec), 25963 name: 'addSourceBuffer' 25964 }); 25965 } 25966 /** 25967 * call abort on a source buffer. 25968 * 25969 * @param {string} type 25970 * The type of source buffer to call abort on. 25971 */ 25972 25973 25974 abort(type) { 25975 pushQueue({ 25976 type, 25977 sourceUpdater: this, 25978 action: actions.abort(type), 25979 name: 'abort' 25980 }); 25981 } 25982 /** 25983 * Call removeSourceBuffer and remove a specific type 25984 * of source buffer on the mediaSource. 25985 * 25986 * @param {string} type 25987 * The type of source buffer to remove. 25988 */ 25989 25990 25991 removeSourceBuffer(type) { 25992 if (!this.canRemoveSourceBuffer()) { 25993 videojs__default["default"].log.error('removeSourceBuffer is not supported!'); 25994 return; 25995 } 25996 25997 pushQueue({ 25998 type: 'mediaSource', 25999 sourceUpdater: this, 26000 action: actions.removeSourceBuffer(type), 26001 name: 'removeSourceBuffer' 26002 }); 26003 } 26004 /** 26005 * Whether or not the removeSourceBuffer function is supported 26006 * on the mediaSource. 26007 * 26008 * @return {boolean} 26009 * if removeSourceBuffer can be called. 26010 */ 26011 26012 26013 canRemoveSourceBuffer() { 26014 // As of Firefox 83 removeSourceBuffer 26015 // throws errors, so we report that it does not support this. 26016 return !videojs__default["default"].browser.IS_FIREFOX && window.MediaSource && window.MediaSource.prototype && typeof window.MediaSource.prototype.removeSourceBuffer === 'function'; 26017 } 26018 /** 26019 * Whether or not the changeType function is supported 26020 * on our SourceBuffers. 26021 * 26022 * @return {boolean} 26023 * if changeType can be called. 26024 */ 26025 26026 26027 static canChangeType() { 26028 return window.SourceBuffer && window.SourceBuffer.prototype && typeof window.SourceBuffer.prototype.changeType === 'function'; 26029 } 26030 /** 26031 * Whether or not the changeType function is supported 26032 * on our SourceBuffers. 26033 * 26034 * @return {boolean} 26035 * if changeType can be called. 26036 */ 26037 26038 26039 canChangeType() { 26040 return this.constructor.canChangeType(); 26041 } 26042 /** 26043 * Call the changeType function on a source buffer, given the code and type. 26044 * 26045 * @param {string} type 26046 * The type of source buffer to call changeType on. 26047 * 26048 * @param {string} codec 26049 * The codec string to change type with on the source buffer. 26050 */ 26051 26052 26053 changeType(type, codec) { 26054 if (!this.canChangeType()) { 26055 videojs__default["default"].log.error('changeType is not supported!'); 26056 return; 26057 } 26058 26059 pushQueue({ 26060 type, 26061 sourceUpdater: this, 26062 action: actions.changeType(codec), 26063 name: 'changeType' 26064 }); 26065 } 26066 /** 26067 * Add source buffers with a codec or, if they are already created, 26068 * call changeType on source buffers using changeType. 26069 * 26070 * @param {Object} codecs 26071 * Codecs to switch to 26072 */ 26073 26074 26075 addOrChangeSourceBuffers(codecs) { 26076 if (!codecs || typeof codecs !== 'object' || Object.keys(codecs).length === 0) { 26077 throw new Error('Cannot addOrChangeSourceBuffers to undefined codecs'); 26078 } 26079 26080 Object.keys(codecs).forEach(type => { 26081 const codec = codecs[type]; 26082 26083 if (!this.hasCreatedSourceBuffers()) { 26084 return this.addSourceBuffer(type, codec); 26085 } 26086 26087 if (this.canChangeType()) { 26088 this.changeType(type, codec); 26089 } 26090 }); 26091 } 26092 /** 26093 * Queue an update to append an ArrayBuffer. 26094 * 26095 * @param {MediaObject}
vendor: 20,327 bytes, lines 26095-26777
26095 object containing audioBytes and/or videoBytes 26096 * @param {Function} done the function to call when done 26097 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 26098 */ 26099 26100 26101 appendBuffer(options, doneFn) { 26102 const { 26103 segmentInfo, 26104 type, 26105 bytes 26106 } = options; 26107 this.processedAppend_ = true; 26108 26109 if (type === 'audio' && this.videoBuffer && !this.videoAppendQueued_) { 26110 this.delayedAudioAppendQueue_.push([options, doneFn]); 26111 this.logger_(`delayed audio append of ${bytes.length} until video append`); 26112 return; 26113 } // In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will 26114 // not be fired. This means that the queue will be blocked until the next action 26115 // taken by the segment-loader. Provide a mechanism for segment-loader to handle 26116 // these errors by calling the doneFn with the specific error. 26117 26118 26119 const onError = doneFn; 26120 pushQueue({ 26121 type, 26122 sourceUpdater: this, 26123 action: actions.appendBuffer(bytes, segmentInfo || { 26124 mediaIndex: -1 26125 }, onError), 26126 doneFn, 26127 name: 'appendBuffer' 26128 }); 26129 26130 if (type === 'video') { 26131 this.videoAppendQueued_ = true; 26132 26133 if (!this.delayedAudioAppendQueue_.length) { 26134 return; 26135 } 26136 26137 const queue = this.delayedAudioAppendQueue_.slice(); 26138 this.logger_(`queuing delayed audio ${queue.length} appendBuffers`); 26139 this.delayedAudioAppendQueue_.length = 0; 26140 queue.forEach(que => { 26141 this.appendBuffer.apply(this, que); 26142 }); 26143 } 26144 } 26145 /** 26146 * Get the audio buffer's buffered timerange. 26147 * 26148 * @return {TimeRange} 26149 * The audio buffer's buffered time range 26150 */ 26151 26152 26153 audioBuffered() { 26154 // no media source/source buffer or it isn't in the media sources 26155 // source buffer list 26156 if (!inSourceBuffers(this.mediaSource, this.audioBuffer)) { 26157 return createTimeRanges(); 26158 } 26159 26160 return this.audioBuffer.buffered ? this.audioBuffer.buffered : createTimeRanges(); 26161 } 26162 /** 26163 * Get the video buffer's buffered timerange. 26164 * 26165 * @return {TimeRange} 26166 * The video buffer's buffered time range 26167 */ 26168 26169 26170 videoBuffered() { 26171 // no media source/source buffer or it isn't in the media sources 26172 // source buffer list 26173 if (!inSourceBuffers(this.mediaSource, this.videoBuffer)) { 26174 return createTimeRanges(); 26175 } 26176 26177 return this.videoBuffer.buffered ? this.videoBuffer.buffered : createTimeRanges(); 26178 } 26179 /** 26180 * Get a combined video/audio buffer's buffered timerange. 26181 * 26182 * @return {TimeRange} 26183 * the combined time range 26184 */ 26185 26186 26187 buffered() { 26188 const video = inSourceBuffers(this.mediaSource, this.videoBuffer) ? this.videoBuffer : null; 26189 const audio = inSourceBuffers(this.mediaSource, this.audioBuffer) ? this.audioBuffer : null; 26190 26191 if (audio && !video) { 26192 return this.audioBuffered(); 26193 } 26194 26195 if (video && !audio) { 26196 return this.videoBuffered(); 26197 } 26198 26199 return bufferIntersection(this.audioBuffered(), this.videoBuffered()); 26200 } 26201 /** 26202 * Add a callback to the queue that will set duration on the mediaSource. 26203 * 26204 * @param {number} duration 26205 * The duration to set 26206 * 26207 * @param {Function} [doneFn] 26208 * function to run after duration has been set. 26209 */ 26210 26211 26212 setDuration(duration, doneFn = noop) { 26213 // In order to set the duration on the media source, it's necessary to wait for all 26214 // source buffers to no longer be updating. "If the updating attribute equals true on 26215 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 26216 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 26217 pushQueue({ 26218 type: 'mediaSource', 26219 sourceUpdater: this, 26220 action: actions.duration(duration), 26221 name: 'duration', 26222 doneFn 26223 }); 26224 } 26225 /** 26226 * Add a mediaSource endOfStream call to the queue 26227 * 26228 * @param {Error} [error] 26229 * Call endOfStream with an error 26230 * 26231 * @param {Function} [doneFn] 26232 * A function that should be called when the 26233 * endOfStream call has finished. 26234 */ 26235 26236 26237 endOfStream(error = null, doneFn = noop) { 26238 if (typeof error !== 'string') { 26239 error = undefined; 26240 } // In order to set the duration on the media source, it's necessary to wait for all 26241 // source buffers to no longer be updating. "If the updating attribute equals true on 26242 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 26243 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 26244 26245 26246 pushQueue({ 26247 type: 'mediaSource', 26248 sourceUpdater: this, 26249 action: actions.endOfStream(error), 26250 name: 'endOfStream', 26251 doneFn 26252 }); 26253 } 26254 /** 26255 * Queue an update to remove a time range from the buffer. 26256 * 26257 * @param {number} start where to start the removal 26258 * @param {number} end where to end the removal 26259 * @param {Function} [done=noop] optional callback to be executed when the remove 26260 * operation is complete 26261 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 26262 */ 26263 26264 26265 removeAudio(start, end, done = noop) { 26266 if (!this.audioBuffered().length || this.audioBuffered().end(0) === 0) { 26267 done(); 26268 return; 26269 } 26270 26271 pushQueue({ 26272 type: 'audio', 26273 sourceUpdater: this, 26274 action: actions.remove(start, end), 26275 doneFn: done, 26276 name: 'remove' 26277 }); 26278 } 26279 /** 26280 * Queue an update to remove a time range from the buffer. 26281 * 26282 * @param {number} start where to start the removal 26283 * @param {number} end where to end the removal 26284 * @param {Function} [done=noop] optional callback to be executed when the remove 26285 * operation is complete 26286 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 26287 */ 26288 26289 26290 removeVideo(start, end, done = noop) { 26291 if (!this.videoBuffered().length || this.videoBuffered().end(0) === 0) { 26292 done(); 26293 return; 26294 } 26295 26296 pushQueue({ 26297 type: 'video', 26298 sourceUpdater: this, 26299 action: actions.remove(start, end), 26300 doneFn: done, 26301 name: 'remove' 26302 }); 26303 } 26304 /** 26305 * Whether the underlying sourceBuffer is updating or not 26306 * 26307 * @return {boolean} the updating status of the SourceBuffer 26308 */ 26309 26310 26311 updating() { 26312 // the audio/video source buffer is updating 26313 if (updating('audio', this) || updating('video', this)) { 26314 return true; 26315 } 26316 26317 return false; 26318 } 26319 /** 26320 * Set/get the timestampoffset on the audio SourceBuffer 26321 * 26322 * @return {number} the timestamp offset 26323 */ 26324 26325 26326 audioTimestampOffset(offset) { 26327 if (typeof offset !== 'undefined' && this.audioBuffer && // no point in updating if it's the same 26328 this.audioTimestampOffset_ !== offset) { 26329 pushQueue({ 26330 type: 'audio', 26331 sourceUpdater: this, 26332 action: actions.timestampOffset(offset), 26333 name: 'timestampOffset' 26334 }); 26335 this.audioTimestampOffset_ = offset; 26336 } 26337 26338 return this.audioTimestampOffset_; 26339 } 26340 /** 26341 * Set/get the timestampoffset on the video SourceBuffer 26342 * 26343 * @return {number} the timestamp offset 26344 */ 26345 26346 26347 videoTimestampOffset(offset) { 26348 if (typeof offset !== 'undefined' && this.videoBuffer && // no point in updating if it's the same 26349 this.videoTimestampOffset !== offset) { 26350 pushQueue({ 26351 type: 'video', 26352 sourceUpdater: this, 26353 action: actions.timestampOffset(offset), 26354 name: 'timestampOffset' 26355 }); 26356 this.videoTimestampOffset_ = offset; 26357 } 26358 26359 return this.videoTimestampOffset_; 26360 } 26361 /** 26362 * Add a function to the queue that will be called 26363 * when it is its turn to run in the audio queue. 26364 * 26365 * @param {Function} callback 26366 * The callback to queue. 26367 */ 26368 26369 26370 audioQueueCallback(callback) { 26371 if (!this.audioBuffer) { 26372 return; 26373 } 26374 26375 pushQueue({ 26376 type: 'audio', 26377 sourceUpdater: this, 26378 action: actions.callback(callback), 26379 name: 'callback' 26380 }); 26381 } 26382 /** 26383 * Add a function to the queue that will be called 26384 * when it is its turn to run in the video queue. 26385 * 26386 * @param {Function} callback 26387 * The callback to queue. 26388 */ 26389 26390 26391 videoQueueCallback(callback) { 26392 if (!this.videoBuffer) { 26393 return; 26394 } 26395 26396 pushQueue({ 26397 type: 'video', 26398 sourceUpdater: this, 26399 action: actions.callback(callback), 26400 name: 'callback' 26401 }); 26402 } 26403 /** 26404 * dispose of the source updater and the underlying sourceBuffer 26405 */ 26406 26407 26408 dispose() { 26409 this.trigger('dispose'); 26410 bufferTypes.forEach(type => { 26411 this.abort(type); 26412 26413 if (this.canRemoveSourceBuffer()) { 26414 this.removeSourceBuffer(type); 26415 } else { 26416 this[`${type}QueueCallback`](() => cleanupBuffer(type, this)); 26417 } 26418 }); 26419 this.videoAppendQueued_ = false; 26420 this.delayedAudioAppendQueue_.length = 0; 26421 26422 if (this.sourceopenListener_) { 26423 this.mediaSource.removeEventListener('sourceopen', this.sourceopenListener_); 26424 } 26425 26426 this.off(); 26427 } 26428 26429 } 26430 26431 const uint8ToUtf8 = uintArray => decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray))); 26432 const bufferToHexString = buffer => { 26433 const uInt8Buffer = new Uint8Array(buffer); 26434 return Array.from(uInt8Buffer).map(byte => byte.toString(16).padStart(2, '0')).join(''); 26435 }; 26436 26437 /** 26438 * @file vtt-segment-loader.js 26439 */ 26440 const VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0))); 26441 26442 class NoVttJsError extends Error { 26443 constructor() { 26444 super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.'); 26445 } 26446 26447 } 26448 /** 26449 * An object that manages segment loading and appending. 26450 * 26451 * @class VTTSegmentLoader 26452 * @param {Object} options required and optional options 26453 * @extends videojs.EventTarget 26454 */ 26455 26456 26457 class VTTSegmentLoader extends SegmentLoader { 26458 constructor(settings, options = {}) { 26459 super(settings, options); // SegmentLoader requires a MediaSource be specified or it will throw an error; 26460 // however, VTTSegmentLoader has no need of a media source, so delete the reference 26461 26462 this.mediaSource_ = null; 26463 this.subtitlesTrack_ = null; 26464 this.loaderType_ = 'subtitle'; 26465 this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks; 26466 this.loadVttJs = settings.loadVttJs; // The VTT segment will have its own time mappings. Saving VTT segment timing info in 26467 // the sync controller leads to improper behavior. 26468 26469 this.shouldSaveSegmentTimingInfo_ = false; 26470 } 26471 26472 createTransmuxer_() { 26473 // don't need to transmux any subtitles 26474 return null; 26475 } 26476 /** 26477 * Indicates which time ranges are buffered 26478 * 26479 * @return {TimeRange} 26480 * TimeRange object representing the current buffered ranges 26481 */ 26482 26483 26484 buffered_() { 26485 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) { 26486 return createTimeRanges(); 26487 } 26488 26489 const cues = this.subtitlesTrack_.cues; 26490 const start = cues[0].startTime; 26491 const end = cues[cues.length - 1].startTime; 26492 return createTimeRanges([[start, end]]); 26493 } 26494 /** 26495 * Gets and sets init segment for the provided map 26496 * 26497 * @param {Object} map 26498 * The map object representing the init segment to get or set 26499 * @param {boolean=} set 26500 * If true, the init segment for the provided map should be saved 26501 * @return {Object} 26502 * map object for desired init segment 26503 */ 26504 26505 26506 initSegmentForMap(map, set = false) { 26507 if (!map) { 26508 return null; 26509 } 26510 26511 const id = initSegmentId(map); 26512 let storedMap = this.initSegments_[id]; 26513 26514 if (set && !storedMap && map.bytes) { 26515 // append WebVTT line terminators to the media initialization segment if it exists 26516 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 26517 // requires two or more WebVTT line terminators between the WebVTT header and the 26518 // rest of the file 26519 const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 26520 const combinedSegment = new Uint8Array(combinedByteLength); 26521 combinedSegment.set(map.bytes); 26522 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 26523 this.initSegments_[id] = storedMap = { 26524 resolvedUri: map.resolvedUri, 26525 byterange: map.byterange, 26526 bytes: combinedSegment 26527 }; 26528 } 26529 26530 return storedMap || map; 26531 } 26532 /** 26533 * Returns true if all configuration required for loading is present, otherwise false. 26534 * 26535 * @return {boolean} True if the all configuration is ready for loading 26536 * @private 26537 */ 26538 26539 26540 couldBeginLoading_() { 26541 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 26542 } 26543 /** 26544 * Once all the starting parameters have been specified, begin 26545 * operation. This method should only be invoked from the INIT 26546 * state. 26547 * 26548 * @private 26549 */ 26550 26551 26552 init_() { 26553 this.state = 'READY'; 26554 this.resetEverything(); 26555 return this.monitorBuffer_(); 26556 } 26557 /** 26558 * Set a subtitle track on the segment loader to add subtitles to 26559 * 26560 * @param {TextTrack=} track 26561 * The text track to add loaded subtitles to 26562 * @return {TextTrack} 26563 * Returns the subtitles track 26564 */ 26565 26566 26567 track(track) { 26568 if (typeof track === 'undefined') { 26569 return this.subtitlesTrack_; 26570 } 26571 26572 this.subtitlesTrack_ = track; // if we were unpaused but waiting for a sourceUpdater, start 26573 // buffering now 26574 26575 if (this.state === 'INIT' && this.couldBeginLoading_()) { 26576 this.init_(); 26577 } 26578 26579 return this.subtitlesTrack_; 26580 } 26581 /** 26582 * Remove any data in the source buffer between start and end times 26583 * 26584 * @param {number} start - the start time of the region to remove from the buffer 26585 * @param {number} end - the end time of the region to remove from the buffer 26586 */ 26587 26588 26589 remove(start, end) { 26590 removeCuesFromTrack(start, end, this.subtitlesTrack_); 26591 } 26592 /** 26593 * fill the buffer with segements unless the sourceBuffers are 26594 * currently updating 26595 * 26596 * Note: this function should only ever be called by monitorBuffer_ 26597 * and never directly 26598 * 26599 * @private 26600 */ 26601 26602 26603 fillBuffer_() { 26604 // see if we need to begin loading immediately 26605 const segmentInfo = this.chooseNextRequest_(); 26606 26607 if (!segmentInfo) { 26608 return; 26609 } 26610 26611 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 26612 // We don't have the timestamp offset that we need to sync subtitles. 26613 // Rerun on a timestamp offset or user interaction. 26614 const checkTimestampOffset = () => { 26615 this.state = 'READY'; 26616 26617 if (!this.paused()) { 26618 // if not paused, queue a buffer check as soon as possible 26619 this.monitorBuffer_(); 26620 } 26621 }; 26622 26623 this.syncController_.one('timestampoffset', checkTimestampOffset); 26624 this.state = 'WAITING_ON_TIMELINE'; 26625 return; 26626 } 26627 26628 this.loadSegment_(segmentInfo); 26629 } // never set a timestamp offset for vtt segments. 26630 26631 26632 timestampOffsetForSegment_() { 26633 return null; 26634 } 26635 26636 chooseNextRequest_() { 26637 return this.skipEmptySegments_(super.chooseNextRequest_()); 26638 } 26639 /** 26640 * Prevents the segment loader from requesting segments we know contain no subtitles 26641 * by walking forward until we find the next segment that we don't know whether it is 26642 * empty or not. 26643 * 26644 * @param {Object} segmentInfo 26645 * a segment info object that describes the current segment 26646 * @return {Object} 26647 * a segment info object that describes the current segment 26648 */ 26649 26650 26651 skipEmptySegments_(segmentInfo) { 26652 while (segmentInfo && segmentInfo.segment.empty) { 26653 // stop at the last possible segmentInfo 26654 if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) { 26655 segmentInfo = null; 26656 break; 26657 } 26658 26659 segmentInfo = this.generateSegmentInfo_({ 26660 playlist: segmentInfo.playlist, 26661 mediaIndex: segmentInfo.mediaIndex + 1, 26662 startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration, 26663 isSyncRequest: segmentInfo.isSyncRequest 26664 }); 26665 } 26666 26667 return segmentInfo; 26668 } 26669 26670 stopForError(error) { 26671 this.error(error); 26672 this.state = 'READY'; 26673 this.pause(); 26674 this.trigger('error'); 26675 } 26676 /** 26677 * append a decrypted segement to the SourceBuffer through a SourceUpdater 26678 * 26679 * @private 26680 */ 26681 26682 26683 segmentRequestFinished_(error, simpleSegment, result) { 26684 if (!this.subtitlesTrack_) { 26685 this.state = 'READY'; 26686 return; 26687 } 26688 26689 this.saveTransferStats_(simpleSegment.stats); // the request was aborted 26690 26691 if (!this.pendingSegment_) { 26692 this.state = 'READY'; 26693 this.mediaRequestsAborted += 1; 26694 return; 26695 } 26696 26697 if (error) { 26698 if (error.code === REQUEST_ERRORS.TIMEOUT) { 26699 this.handleTimeout_(); 26700 } 26701 26702 if (error.code === REQUEST_ERRORS.ABORTED) { 26703 this.mediaRequestsAborted += 1; 26704 } else { 26705 this.mediaRequestsErrored += 1; 26706 } 26707 26708 this.stopForError(error); 26709 return; 26710 } 26711 26712 const segmentInfo = this.pendingSegment_; // although the VTT segment loader bandwidth isn't really used, it's good to 26713 // maintain functionality between segment loaders 26714 26715 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); // if this request included a segment key, save that data in the cache 26716 26717 if (simpleSegment.key) { 26718 this.segmentKey(simpleSegment.key, true); 26719 } 26720 26721 this.state = 'APPENDING'; // used for tests 26722 26723 this.trigger('appending'); 26724 const segment = segmentInfo.segment; 26725 26726 if (segment.map) { 26727 segment.map.bytes = simpleSegment.map.bytes; 26728 } 26729 26730 segmentInfo.bytes = simpleSegment.bytes; // Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading 26731 26732 if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') { 26733 this.state = 'WAITING_ON_VTTJS'; // should be fine to call multiple times 26734 // script will be loaded once but multiple listeners will be added to the queue, which is expected. 26735 26736 this.loadVttJs().then(() => this.segmentRequestFinished_(error, simpleSegment, result), () => this.stopForError({ 26737 message: 'Error loading vtt.js' 26738 })); 26739 return; 26740 } 26741 26742 segment.requested = true; 26743 26744 try { 26745 this.parseVTTCues_(segmentInfo); 26746 } catch (e) { 26747 this.stopForError({ 26748 message: e.message 26749 }); 26750 return; 26751 } 26752 26753 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_); 26754 26755 if (segmentInfo.cues.length) { 26756 segmentInfo.timingInfo = { 26757 start: segmentInfo.cues[0].startTime, 26758 end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime 26759 }; 26760 } else { 26761 segmentInfo.timingInfo = { 26762 start: segmentInfo.startOfSegment, 26763 end: segmentInfo.startOfSegment + segmentInfo.duration 26764 }; 26765 } 26766 26767 if (segmentInfo.isSyncRequest) { 26768 this.trigger('syncinfoupdate'); 26769 this.pendingSegment_ = null; 26770 this.state = 'READY'; 26771 return; 26772 } 26773 26774 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 26775 this.mediaSecondsLoaded += segment.duration; // Create VTTCue instances for each cue in the new segment and add them to 26776 // the subtitle track 26777
vendor: 17,619 bytes, lines 26778-27314
26778 segmentInfo.cues.forEach(cue => { 26779 this.subtitlesTrack_.addCue(this.featuresNativeTextTracks_ ? new window.VTTCue(cue.startTime, cue.endTime, cue.text) : cue); 26780 }); // Remove any duplicate cues from the subtitle track. The WebVTT spec allows 26781 // cues to have identical time-intervals, but if the text is also identical 26782 // we can safely assume it is a duplicate that can be removed (ex. when a cue 26783 // "overlaps" VTT segments) 26784 26785 removeDuplicateCuesFromTrack(this.subtitlesTrack_); 26786 this.handleAppendsDone_(); 26787 } 26788 26789 handleData_() {// noop as we shouldn't be getting video/audio data captions 26790 // that we do not support here. 26791 } 26792 26793 updateTimingInfoEnd_() {// noop 26794 } 26795 /** 26796 * Uses the WebVTT parser to parse the segment response 26797 * 26798 * @throws NoVttJsError 26799 * 26800 * @param {Object} segmentInfo 26801 * a segment info object that describes the current segment 26802 * @private 26803 */ 26804 26805 26806 parseVTTCues_(segmentInfo) { 26807 let decoder; 26808 let decodeBytesToString = false; 26809 26810 if (typeof window.WebVTT !== 'function') { 26811 // caller is responsible for exception handling. 26812 throw new NoVttJsError(); 26813 } 26814 26815 if (typeof window.TextDecoder === 'function') { 26816 decoder = new window.TextDecoder('utf8'); 26817 } else { 26818 decoder = window.WebVTT.StringDecoder(); 26819 decodeBytesToString = true; 26820 } 26821 26822 const parser = new window.WebVTT.Parser(window, window.vttjs, decoder); 26823 segmentInfo.cues = []; 26824 segmentInfo.timestampmap = { 26825 MPEGTS: 0, 26826 LOCAL: 0 26827 }; 26828 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 26829 26830 parser.ontimestampmap = map => { 26831 segmentInfo.timestampmap = map; 26832 }; 26833 26834 parser.onparsingerror = error => { 26835 videojs__default["default"].log.warn('Error encountered when parsing cues: ' + error.message); 26836 }; 26837 26838 if (segmentInfo.segment.map) { 26839 let mapData = segmentInfo.segment.map.bytes; 26840 26841 if (decodeBytesToString) { 26842 mapData = uint8ToUtf8(mapData); 26843 } 26844 26845 parser.parse(mapData); 26846 } 26847 26848 let segmentData = segmentInfo.bytes; 26849 26850 if (decodeBytesToString) { 26851 segmentData = uint8ToUtf8(segmentData); 26852 } 26853 26854 parser.parse(segmentData); 26855 parser.flush(); 26856 } 26857 /** 26858 * Updates the start and end times of any cues parsed by the WebVTT parser using 26859 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 26860 * from the SyncController 26861 * 26862 * @param {Object} segmentInfo 26863 * a segment info object that describes the current segment 26864 * @param {Object} mappingObj 26865 * object containing a mapping from TS to media time 26866 * @param {Object} playlist 26867 * the playlist object containing the segment 26868 * @private 26869 */ 26870 26871 26872 updateTimeMapping_(segmentInfo, mappingObj, playlist) { 26873 const segment = segmentInfo.segment; 26874 26875 if (!mappingObj) { 26876 // If the sync controller does not have a mapping of TS to Media Time for the 26877 // timeline, then we don't have enough information to update the cue 26878 // start/end times 26879 return; 26880 } 26881 26882 if (!segmentInfo.cues.length) { 26883 // If there are no cues, we also do not have enough information to figure out 26884 // segment timing. Mark that the segment contains no cues so we don't re-request 26885 // an empty segment. 26886 segment.empty = true; 26887 return; 26888 } 26889 26890 const { 26891 MPEGTS, 26892 LOCAL 26893 } = segmentInfo.timestampmap; 26894 /** 26895 * From the spec: 26896 * The MPEGTS media timestamp MUST use a 90KHz timescale, 26897 * even when non-WebVTT Media Segments use a different timescale. 26898 */ 26899 26900 const mpegTsInSeconds = MPEGTS / clock.ONE_SECOND_IN_TS; 26901 const diff = mpegTsInSeconds - LOCAL + mappingObj.mapping; 26902 segmentInfo.cues.forEach(cue => { 26903 const duration = cue.endTime - cue.startTime; 26904 const startTime = MPEGTS === 0 ? cue.startTime + diff : this.handleRollover_(cue.startTime + diff, mappingObj.time); 26905 cue.startTime = Math.max(startTime, 0); 26906 cue.endTime = Math.max(startTime + duration, 0); 26907 }); 26908 26909 if (!playlist.syncInfo) { 26910 const firstStart = segmentInfo.cues[0].startTime; 26911 const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 26912 playlist.syncInfo = { 26913 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 26914 time: Math.min(firstStart, lastStart - segment.duration) 26915 }; 26916 } 26917 } 26918 /** 26919 * MPEG-TS PES timestamps are limited to 2^33. 26920 * Once they reach 2^33, they roll over to 0. 26921 * mux.js handles PES timestamp rollover for the following scenarios: 26922 * [forward rollover(right)] -> 26923 * PES timestamps monotonically increase, and once they reach 2^33, they roll over to 0 26924 * [backward rollover(left)] --> 26925 * we seek back to position before rollover. 26926 * 26927 * According to the HLS SPEC: 26928 * When synchronizing WebVTT with PES timestamps, clients SHOULD account 26929 * for cases where the 33-bit PES timestamps have wrapped and the WebVTT 26930 * cue times have not. When the PES timestamp wraps, the WebVTT Segment 26931 * SHOULD have a X-TIMESTAMP-MAP header that maps the current WebVTT 26932 * time to the new (low valued) PES timestamp. 26933 * 26934 * So we want to handle rollover here and align VTT Cue start/end time to the player's time. 26935 */ 26936 26937 26938 handleRollover_(value, reference) { 26939 if (reference === null) { 26940 return value; 26941 } 26942 26943 let valueIn90khz = value * clock.ONE_SECOND_IN_TS; 26944 const referenceIn90khz = reference * clock.ONE_SECOND_IN_TS; 26945 let offset; 26946 26947 if (referenceIn90khz < valueIn90khz) { 26948 // - 2^33 26949 offset = -8589934592; 26950 } else { 26951 // + 2^33 26952 offset = 8589934592; 26953 } // distance(value - reference) > 2^32 26954 26955 26956 while (Math.abs(valueIn90khz - referenceIn90khz) > 4294967296) { 26957 valueIn90khz += offset; 26958 } 26959 26960 return valueIn90khz / clock.ONE_SECOND_IN_TS; 26961 } 26962 26963 } 26964 26965 /** 26966 * @file ad-cue-tags.js 26967 */ 26968 26969 /** 26970 * Searches for an ad cue that overlaps with the given mediaTime 26971 * 26972 * @param {Object} track 26973 * the track to find the cue for 26974 * 26975 * @param {number} mediaTime 26976 * the time to find the cue at 26977 * 26978 * @return {Object|null} 26979 * the found cue or null 26980 */ 26981 const findAdCue = function (track, mediaTime) { 26982 const cues = track.cues; 26983 26984 for (let i = 0; i < cues.length; i++) { 26985 const cue = cues[i]; 26986 26987 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 26988 return cue; 26989 } 26990 } 26991 26992 return null; 26993 }; 26994 const updateAdCues = function (media, track, offset = 0) { 26995 if (!media.segments) { 26996 return; 26997 } 26998 26999 let mediaTime = offset; 27000 let cue; 27001 27002 for (let i = 0; i < media.segments.length; i++) { 27003 const segment = media.segments[i]; 27004 27005 if (!cue) { 27006 // Since the cues will span for at least the segment duration, adding a fudge 27007 // factor of half segment duration will prevent duplicate cues from being 27008 // created when timing info is not exact (e.g. cue start time initialized 27009 // at 10.006677, but next call mediaTime is 10.003332 ) 27010 cue = findAdCue(track, mediaTime + segment.duration / 2); 27011 } 27012 27013 if (cue) { 27014 if ('cueIn' in segment) { 27015 // Found a CUE-IN so end the cue 27016 cue.endTime = mediaTime; 27017 cue.adEndTime = mediaTime; 27018 mediaTime += segment.duration; 27019 cue = null; 27020 continue; 27021 } 27022 27023 if (mediaTime < cue.endTime) { 27024 // Already processed this mediaTime for this cue 27025 mediaTime += segment.duration; 27026 continue; 27027 } // otherwise extend cue until a CUE-IN is found 27028 27029 27030 cue.endTime += segment.duration; 27031 } else { 27032 if ('cueOut' in segment) { 27033 cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); 27034 cue.adStartTime = mediaTime; // Assumes tag format to be 27035 // #EXT-X-CUE-OUT:30 27036 27037 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 27038 track.addCue(cue); 27039 } 27040 27041 if ('cueOutCont' in segment) { 27042 // Entered into the middle of an ad cue 27043 // Assumes tag formate to be 27044 // #EXT-X-CUE-OUT-CONT:10/30 27045 const [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat); 27046 cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, ''); 27047 cue.adStartTime = mediaTime - adOffset; 27048 cue.adEndTime = cue.adStartTime + adTotal; 27049 track.addCue(cue); 27050 } 27051 } 27052 27053 mediaTime += segment.duration; 27054 } 27055 }; 27056 27057 /** 27058 * @file sync-controller.js 27059 */ 27060 // synchronize expired playlist segments. 27061 // the max media sequence diff is 48 hours of live stream 27062 // content with two second segments. Anything larger than that 27063 // will likely be invalid. 27064 27065 const MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC = 86400; 27066 const syncPointStrategies = [// Stategy "VOD": Handle the VOD-case where the sync-point is *always* 27067 // the equivalence display-time 0 === segment-index 0 27068 { 27069 name: 'VOD', 27070 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 27071 if (duration !== Infinity) { 27072 const syncPoint = { 27073 time: 0, 27074 segmentIndex: 0, 27075 partIndex: null 27076 }; 27077 return syncPoint; 27078 } 27079 27080 return null; 27081 } 27082 }, { 27083 name: 'MediaSequence', 27084 27085 /** 27086 * run media sequence strategy 27087 * 27088 * @param {SyncController} syncController 27089 * @param {Object} playlist 27090 * @param {number} duration 27091 * @param {number} currentTimeline 27092 * @param {number} currentTime 27093 * @param {string} type 27094 */ 27095 run: (syncController, playlist, duration, currentTimeline, currentTime, type) => { 27096 if (!type) { 27097 return null; 27098 } 27099 27100 const mediaSequenceMap = syncController.getMediaSequenceMap(type); 27101 27102 if (!mediaSequenceMap || mediaSequenceMap.size === 0) { 27103 return null; 27104 } 27105 27106 if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) { 27107 return null; 27108 } 27109 27110 let currentMediaSequence = playlist.mediaSequence; 27111 let segmentIndex = 0; 27112 27113 for (const segment of playlist.segments) { 27114 const range = mediaSequenceMap.get(currentMediaSequence); 27115 27116 if (!range) { 27117 // unexpected case 27118 // we expect this playlist to be the same playlist in the map 27119 // just break from the loop and move forward to the next strategy 27120 break; 27121 } 27122 27123 if (currentTime >= range.start && currentTime < range.end) { 27124 // we found segment 27125 if (Array.isArray(segment.parts) && segment.parts.length) { 27126 let currentPartStart = range.start; 27127 let partIndex = 0; 27128 27129 for (const part of segment.parts) { 27130 const start = currentPartStart; 27131 const end = start + part.duration; 27132 27133 if (currentTime >= start && currentTime < end) { 27134 return { 27135 time: range.start, 27136 segmentIndex, 27137 partIndex 27138 }; 27139 } 27140 27141 partIndex++; 27142 currentPartStart = end; 27143 } 27144 } // no parts found, return sync point for segment 27145 27146 27147 return { 27148 time: range.start, 27149 segmentIndex, 27150 partIndex: null 27151 }; 27152 } 27153 27154 segmentIndex++; 27155 currentMediaSequence++; 27156 } // we didn't find any segments for provided current time 27157 27158 27159 return null; 27160 } 27161 }, // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 27162 { 27163 name: 'ProgramDateTime', 27164 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 27165 if (!Object.keys(syncController.timelineToDatetimeMappings).length) { 27166 return null; 27167 } 27168 27169 let syncPoint = null; 27170 let lastDistance = null; 27171 const partsAndSegments = getPartsAndSegments(playlist); 27172 currentTime = currentTime || 0; 27173 27174 for (let i = 0; i < partsAndSegments.length; i++) { 27175 // start from the end and loop backwards for live 27176 // or start from the front and loop forwards for non-live 27177 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 27178 const partAndSegment = partsAndSegments[index]; 27179 const segment = partAndSegment.segment; 27180 const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline]; 27181 27182 if (!datetimeMapping || !segment.dateTimeObject) { 27183 continue; 27184 } 27185 27186 const segmentTime = segment.dateTimeObject.getTime() / 1000; 27187 let start = segmentTime + datetimeMapping; // take part duration into account. 27188 27189 if (segment.parts && typeof partAndSegment.partIndex === 'number') { 27190 for (let z = 0; z < partAndSegment.partIndex; z++) { 27191 start += segment.parts[z].duration; 27192 } 27193 } 27194 27195 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed 27196 // currentTime and can stop looking for better candidates 27197 27198 if (lastDistance !== null && (distance === 0 || lastDistance < distance)) { 27199 break; 27200 } 27201 27202 lastDistance = distance; 27203 syncPoint = { 27204 time: start, 27205 segmentIndex: partAndSegment.segmentIndex, 27206 partIndex: partAndSegment.partIndex 27207 }; 27208 } 27209 27210 return syncPoint; 27211 } 27212 }, // Stategy "Segment": We have a known time mapping for a timeline and a 27213 // segment in the current timeline with timing data 27214 { 27215 name: 'Segment', 27216 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 27217 let syncPoint = null; 27218 let lastDistance = null; 27219 currentTime = currentTime || 0; 27220 const partsAndSegments = getPartsAndSegments(playlist); 27221 27222 for (let i = 0; i < partsAndSegments.length; i++) { 27223 // start from the end and loop backwards for live 27224 // or start from the front and loop forwards for non-live 27225 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 27226 const partAndSegment = partsAndSegments[index]; 27227 const segment = partAndSegment.segment; 27228 const start = partAndSegment.part && partAndSegment.part.start || segment && segment.start; 27229 27230 if (segment.timeline === currentTimeline && typeof start !== 'undefined') { 27231 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed 27232 // currentTime and can stop looking for better candidates 27233 27234 if (lastDistance !== null && lastDistance < distance) { 27235 break; 27236 } 27237 27238 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 27239 lastDistance = distance; 27240 syncPoint = { 27241 time: start, 27242 segmentIndex: partAndSegment.segmentIndex, 27243 partIndex: partAndSegment.partIndex 27244 }; 27245 } 27246 } 27247 } 27248 27249 return syncPoint; 27250 } 27251 }, // Stategy "Discontinuity": We have a discontinuity with a known 27252 // display-time 27253 { 27254 name: 'Discontinuity', 27255 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 27256 let syncPoint = null; 27257 currentTime = currentTime || 0; 27258 27259 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 27260 let lastDistance = null; 27261 27262 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 27263 const segmentIndex = playlist.discontinuityStarts[i]; 27264 const discontinuity = playlist.discontinuitySequence + i + 1; 27265 const discontinuitySync = syncController.discontinuities[discontinuity]; 27266 27267 if (discontinuitySync) { 27268 const distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed 27269 // currentTime and can stop looking for better candidates 27270 27271 if (lastDistance !== null && lastDistance < distance) { 27272 break; 27273 } 27274 27275 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 27276 lastDistance = distance; 27277 syncPoint = { 27278 time: discontinuitySync.time, 27279 segmentIndex, 27280 partIndex: null 27281 }; 27282 } 27283 } 27284 } 27285 } 27286 27287 return syncPoint; 27288 } 27289 }, // Stategy "Playlist": We have a playlist with a known mapping of 27290 // segment index to display time 27291 { 27292 name: 'Playlist', 27293 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 27294 if (playlist.syncInfo) { 27295 const syncPoint = { 27296 time: playlist.syncInfo.time, 27297 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence, 27298 partIndex: null 27299 }; 27300 return syncPoint; 27301 } 27302 27303 return null; 27304 } 27305 }]; 27306 class SyncController extends videojs__default["default"].EventTarget { 27307 constructor(options = {}) { 27308 super(); // ...for synching across variants 27309 27310 this.timelines = []; 27311 this.discontinuities = []; 27312 this.timelineToDatetimeMappings = {}; 27313 /** 27314 * @type {Map<string, Map<number, { start: number, end: number }>
27314>} 27315 * @private 27316 */ 27317 27318 this.mediaSequenceStorage_ = new Map(); 27319 this.logger_ = logger('SyncController'); 27320 } 27321 /** 27322 * Get media sequence map by type 27323 * 27324 * @param {string} type - segment loader type 27325 * @return {Map<number, { start: number, end: number }> | undefined} 27326 */ 27327 27328 27329 getMediaSequenceMap(type) { 27330 return this.mediaSequenceStorage_.get(type); 27331 } 27332 /** 27333 * Update Media Sequence Map -> <MediaSequence, Range> 27334 * 27335 * @param {Object} playlist - parsed playlist 27336 * @param {number} currentTime - current player's time 27337 * @param {string} type - segment loader type 27338 * @return {void} 27339 */ 27340 27341 27342 updateMediaSequenceMap(playlist, currentTime, type) { 27343 // we should not process this playlist if it does not have mediaSequence or segments 27344 if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) { 27345 return; 27346 } 27347 27348 const currentMap = this.getMediaSequenceMap(type); 27349 const result = new Map(); 27350 let currentMediaSequence = playlist.mediaSequence; 27351 let currentBaseTime; 27352 27353 if (!currentMap) { 27354 // first playlist setup: 27355 currentBaseTime = 0; 27356 } else if (currentMap.has(playlist.mediaSequence)) { 27357 // further playlists setup: 27358 currentBaseTime = currentMap.get(playlist.mediaSequence).start; 27359 } else { 27360 // it seems like we have a gap between playlists, use current time as a fallback: 27361 this.logger_(`MediaSequence sync for ${type} segment loader - received a gap between playlists. 27362Fallback base time to: ${currentTime}. 27363Received media sequence: ${currentMediaSequence}. 27364Current map: `, currentMap); 27365 currentBaseTime = currentTime; 27366 } 27367 27368 this.logger_(`MediaSequence sync for ${type} segment loader. 27369Received media sequence: ${currentMediaSequence}. 27370base time is ${currentBaseTime} 27371Current map: `, currentMap); 27372 playlist.segments.forEach(segment => { 27373 const start = currentBaseTime; 27374 const end = start + segment.duration; 27375 const range = { 27376 start, 27377 end 27378 }; 27379 result.set(currentMediaSequence, range); 27380 currentMediaSequence++; 27381 currentBaseTime = end; 27382 }); 27383 this.mediaSequenceStorage_.set(type, result); 27384 } 27385 /** 27386 * Find a sync-point for the playlist specified 27387 * 27388 * A sync-point is defined as a known mapping from display-time to 27389 * a segment-index in the current playlist. 27390 * 27391 * @param {Playlist} playlist 27392 * The playlist that needs a sync-point 27393 * @param {number} duration 27394 * Duration of the MediaSource (Infinite if playing a live source) 27395 * @param {number} currentTimeline 27396 * The last timeline from which a segment was loaded 27397 * @param {number} currentTime 27398 * Current player's time 27399 * @param {string} type 27400 * Segment loader type 27401 * @return {Object} 27402 * A sync-point object 27403 */ 27404 27405 27406 getSyncPoint(playlist, duration, currentTimeline, currentTime, type) { 27407 // Always use VOD sync point for VOD 27408 if (duration !== Infinity) { 27409 const vodSyncPointStrategy = syncPointStrategies.find(({ 27410 name 27411 }) => name === 'VOD'); 27412 return vodSyncPointStrategy.run(this, playlist, duration); 27413 } 27414 27415 const syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime, type); 27416 27417 if (!syncPoints.length) { 27418 // Signal that we need to attempt to get a sync-point manually 27419 // by fetching a segment in the playlist and constructing 27420 // a sync-point from that information 27421 return null; 27422 } // If we have exact match just return it instead of finding the nearest distance 27423 27424 27425 for (const syncPointInfo of syncPoints) { 27426 const { 27427 syncPoint, 27428 strategy 27429 } = syncPointInfo; 27430 const { 27431 segmentIndex, 27432 time 27433 } = syncPoint; 27434 27435 if (segmentIndex < 0) { 27436 continue; 27437 } 27438 27439 const selectedSegment = playlist.segments[segmentIndex]; 27440 const start = time; 27441 const end = start + selectedSegment.duration; 27442 this.logger_(`Strategy: ${strategy}. Current time: ${currentTime}. selected segment: ${segmentIndex}. Time: [${start} -> ${end}]}`); 27443 27444 if (currentTime >= start && currentTime < end) { 27445 this.logger_('Found sync point with exact match: ', syncPoint); 27446 return syncPoint; 27447 } 27448 } // Now find the sync-point that is closest to the currentTime because 27449 // that should result in the most accurate guess about which segment 27450 // to fetch 27451 27452 27453 return this.selectSyncPoint_(syncPoints, { 27454 key: 'time', 27455 value: currentTime 27456 }); 27457 } 27458 /** 27459 * Calculate the amount of time that has expired off the playlist during playback 27460 * 27461 * @param {Playlist} playlist 27462 * Playlist object to calculate expired from 27463 * @param {number} duration 27464 * Duration of the MediaSource (Infinity if playling a live source) 27465 * @return {number|null} 27466 * The amount of time that has expired off the playlist during playback. Null 27467 * if no sync-points for the playlist can be found. 27468 */ 27469 27470 27471 getExpiredTime(playlist, duration) { 27472 if (!playlist || !playlist.segments) { 27473 return null; 27474 } 27475 27476 const syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0, 'main'); // Without sync-points, there is not enough information to determine the expired time 27477 27478 if (!syncPoints.length) { 27479 return null; 27480 } 27481 27482 const syncPoint = this.selectSyncPoint_(syncPoints, { 27483 key: 'segmentIndex', 27484 value: 0 27485 }); // If the sync-point is beyond the start of the playlist, we want to subtract the 27486 // duration from index 0 to syncPoint.segmentIndex instead of adding. 27487 27488 if (syncPoint.segmentIndex > 0) { 27489 syncPoint.time *= -1; 27490 } 27491 27492 return Math.abs(syncPoint.time + sumDurations({ 27493 defaultDuration: playlist.targetDuration, 27494 durationList: playlist.segments, 27495 startIndex: syncPoint.segmentIndex, 27496 endIndex: 0 27497 })); 27498 } 27499 /** 27500 * Runs each sync-point strategy and returns a list of sync-points returned by the 27501 * strategies 27502 * 27503 * @private 27504 * @param {Playlist} playlist 27505 * The playlist that needs a sync-point 27506 * @param {number} duration 27507 * Duration of the MediaSource (Infinity if playing a live source) 27508 * @param {number} currentTimeline 27509 * The last timeline from which a segment was loaded 27510 * @param {number} currentTime
vendor: 8,283 bytes, lines 27511-27725
27511 * Current player's time 27512 * @param {string} type 27513 * Segment loader type 27514 * @return {Array} 27515 * A list of sync-point objects 27516 */ 27517 27518 27519 runStrategies_(playlist, duration, currentTimeline, currentTime, type) { 27520 const syncPoints = []; // Try to find a sync-point in by utilizing various strategies... 27521 27522 for (let i = 0; i < syncPointStrategies.length; i++) { 27523 const strategy = syncPointStrategies[i]; 27524 const syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime, type); 27525 27526 if (syncPoint) { 27527 syncPoint.strategy = strategy.name; 27528 syncPoints.push({ 27529 strategy: strategy.name, 27530 syncPoint 27531 }); 27532 } 27533 } 27534 27535 return syncPoints; 27536 } 27537 /** 27538 * Selects the sync-point nearest the specified target 27539 * 27540 * @private 27541 * @param {Array} syncPoints 27542 * List of sync-points to select from 27543 * @param {Object} target 27544 * Object specifying the property and value we are targeting 27545 * @param {string} target.key 27546 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 27547 * @param {number} target.value 27548 * The value to target for the specified key. 27549 * @return {Object} 27550 * The sync-point nearest the target 27551 */ 27552 27553 27554 selectSyncPoint_(syncPoints, target) { 27555 let bestSyncPoint = syncPoints[0].syncPoint; 27556 let bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 27557 let bestStrategy = syncPoints[0].strategy; 27558 27559 for (let i = 1; i < syncPoints.length; i++) { 27560 const newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 27561 27562 if (newDistance < bestDistance) { 27563 bestDistance = newDistance; 27564 bestSyncPoint = syncPoints[i].syncPoint; 27565 bestStrategy = syncPoints[i].strategy; 27566 } 27567 } 27568 27569 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}` : '') + ']'); 27570 return bestSyncPoint; 27571 } 27572 /** 27573 * Save any meta-data present on the segments when segments leave 27574 * the live window to the playlist to allow for synchronization at the 27575 * playlist level later. 27576 * 27577 * @param {Playlist} oldPlaylist - The previous active playlist 27578 * @param {Playlist} newPlaylist - The updated and most current playlist 27579 */ 27580 27581 27582 saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 27583 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // Ignore large media sequence gaps 27584 27585 if (mediaSequenceDiff > MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC) { 27586 videojs__default["default"].log.warn(`Not saving expired segment info. Media sequence gap ${mediaSequenceDiff} is too large.`); 27587 return; 27588 } // When a segment expires from the playlist and it has a start time 27589 // save that information as a possible sync-point reference in future 27590 27591 27592 for (let i = mediaSequenceDiff - 1; i >= 0; i--) { 27593 const lastRemovedSegment = oldPlaylist.segments[i]; 27594 27595 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 27596 newPlaylist.syncInfo = { 27597 mediaSequence: oldPlaylist.mediaSequence + i, 27598 time: lastRemovedSegment.start 27599 }; 27600 this.logger_(`playlist refresh sync: [time:${newPlaylist.syncInfo.time},` + ` mediaSequence: ${newPlaylist.syncInfo.mediaSequence}]`); 27601 this.trigger('syncinfoupdate'); 27602 break; 27603 } 27604 } 27605 } 27606 /** 27607 * Save the mapping from playlist's ProgramDateTime to display. This should only happen 27608 * before segments start to load. 27609 * 27610 * @param {Playlist} playlist - The currently active playlist 27611 */ 27612 27613 27614 setDateTimeMappingForStart(playlist) { 27615 // It's possible for the playlist to be updated before playback starts, meaning time 27616 // zero is not yet set. If, during these playlist refreshes, a discontinuity is 27617 // crossed, then the old time zero mapping (for the prior timeline) would be retained 27618 // unless the mappings are cleared. 27619 this.timelineToDatetimeMappings = {}; 27620 27621 if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 27622 const firstSegment = playlist.segments[0]; 27623 const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000; 27624 this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp; 27625 } 27626 } 27627 /** 27628 * Calculates and saves timeline mappings, playlist sync info, and segment timing values 27629 * based on the latest timing information. 27630 * 27631 * @param {Object} options 27632 * Options object 27633 * @param {SegmentInfo} options.segmentInfo 27634 * The current active request information 27635 * @param {boolean} options.shouldSaveTimelineMapping 27636 * If there's a timeline change, determines if the timeline mapping should be 27637 * saved for timeline mapping and program date time mappings. 27638 */ 27639 27640 27641 saveSegmentTimingInfo({ 27642 segmentInfo, 27643 shouldSaveTimelineMapping 27644 }) { 27645 const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_(segmentInfo, segmentInfo.timingInfo, shouldSaveTimelineMapping); 27646 const segment = segmentInfo.segment; 27647 27648 if (didCalculateSegmentTimeMapping) { 27649 this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information 27650 // now with segment timing information 27651 27652 if (!segmentInfo.playlist.syncInfo) { 27653 segmentInfo.playlist.syncInfo = { 27654 mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex, 27655 time: segment.start 27656 }; 27657 } 27658 } 27659 27660 const dateTime = segment.dateTimeObject; 27661 27662 if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) { 27663 this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000); 27664 } 27665 } 27666 27667 timestampOffsetForTimeline(timeline) { 27668 if (typeof this.timelines[timeline] === 'undefined') { 27669 return null; 27670 } 27671 27672 return this.timelines[timeline].time; 27673 } 27674 27675 mappingForTimeline(timeline) { 27676 if (typeof this.timelines[timeline] === 'undefined') { 27677 return null; 27678 } 27679 27680 return this.timelines[timeline].mapping; 27681 } 27682 /** 27683 * Use the "media time" for a segment to generate a mapping to "display time" and 27684 * save that display time to the segment. 27685 * 27686 * @private 27687 * @param {SegmentInfo} segmentInfo 27688 * The current active request information 27689 * @param {Object} timingInfo 27690 * The start and end time of the current segment in "media time" 27691 * @param {boolean} shouldSaveTimelineMapping 27692 * If there's a timeline change, determines if the timeline mapping should be 27693 * saved in timelines. 27694 * @return {boolean} 27695 * Returns false if segment time mapping could not be calculated 27696 */ 27697 27698 27699 calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) { 27700 // TODO: remove side effects 27701 const segment = segmentInfo.segment; 27702 const part = segmentInfo.part; 27703 let mappingObj = this.timelines[segmentInfo.timeline]; 27704 let start; 27705 let end; 27706 27707 if (typeof segmentInfo.timestampOffset === 'number') { 27708 mappingObj = { 27709 time: segmentInfo.startOfSegment, 27710 mapping: segmentInfo.startOfSegment - timingInfo.start 27711 }; 27712 27713 if (shouldSaveTimelineMapping) { 27714 this.timelines[segmentInfo.timeline] = mappingObj; 27715 this.trigger('timestampoffset'); 27716 this.logger_(`time mapping for timeline ${segmentInfo.timeline}: ` + `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]`); 27717 } 27718 27719 start = segmentInfo.startOfSegment; 27720 end = timingInfo.end + mappingObj.mapping; 27721 } else if (mappingObj) { 27722 start = timingInfo.start + mappingObj.mapping; 27723 end = timingInfo.end + mappingObj.mapping; 27724 } else { 27725 return false;
vendor: 5,983 bytes, lines 27726-27922
27726 } 27727 27728 if (part) { 27729 part.start = start; 27730 part.end = end; 27731 } // If we don't have a segment start yet or the start value we got 27732 // is less than our current segment.start value, save a new start value. 27733 // We have to do this because parts will have segment timing info saved 27734 // multiple times and we want segment start to be the earliest part start 27735 // value for that segment. 27736 27737 27738 if (!segment.start || start < segment.start) { 27739 segment.start = start; 27740 } 27741 27742 segment.end = end; 27743 return true; 27744 } 27745 /** 27746 * Each time we have discontinuity in the playlist, attempt to calculate the location 27747 * in display of the start of the discontinuity and save that. We also save an accuracy 27748 * value so that we save values with the most accuracy (closest to 0.) 27749 * 27750 * @private 27751 * @param {SegmentInfo} segmentInfo - The current active request information 27752 */ 27753 27754 27755 saveDiscontinuitySyncInfo_(segmentInfo) { 27756 const playlist = segmentInfo.playlist; 27757 const segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where 27758 // the start of the range and it's accuracy is 0 (greater accuracy values 27759 // mean more approximation) 27760 27761 if (segment.discontinuity) { 27762 this.discontinuities[segment.timeline] = { 27763 time: segment.start, 27764 accuracy: 0 27765 }; 27766 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 27767 // Search for future discontinuities that we can provide better timing 27768 // information for and save that information for sync purposes 27769 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 27770 const segmentIndex = playlist.discontinuityStarts[i]; 27771 const discontinuity = playlist.discontinuitySequence + i + 1; 27772 const mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 27773 const accuracy = Math.abs(mediaIndexDiff); 27774 27775 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) { 27776 let time; 27777 27778 if (mediaIndexDiff < 0) { 27779 time = segment.start - sumDurations({ 27780 defaultDuration: playlist.targetDuration, 27781 durationList: playlist.segments, 27782 startIndex: segmentInfo.mediaIndex, 27783 endIndex: segmentIndex 27784 }); 27785 } else { 27786 time = segment.end + sumDurations({ 27787 defaultDuration: playlist.targetDuration, 27788 durationList: playlist.segments, 27789 startIndex: segmentInfo.mediaIndex + 1, 27790 endIndex: segmentIndex 27791 }); 27792 } 27793 27794 this.discontinuities[discontinuity] = { 27795 time, 27796 accuracy 27797 }; 27798 } 27799 } 27800 } 27801 } 27802 27803 dispose() { 27804 this.trigger('dispose'); 27805 this.off(); 27806 } 27807 27808 } 27809 27810 /** 27811 * The TimelineChangeController acts as a source for segment loaders to listen for and 27812 * keep track of latest and pending timeline changes. This is useful to ensure proper 27813 * sync, as each loader may need to make a consideration for what timeline the other 27814 * loader is on before making changes which could impact the other loader's media. 27815 * 27816 * @class TimelineChangeController 27817 * @extends videojs.EventTarget 27818 */ 27819 27820 class TimelineChangeController extends videojs__default["default"].EventTarget { 27821 constructor() { 27822 super(); 27823 this.pendingTimelineChanges_ = {}; 27824 this.lastTimelineChanges_ = {}; 27825 } 27826 27827 clearPendingTimelineChange(type) { 27828 this.pendingTimelineChanges_[type] = null; 27829 this.trigger('pendingtimelinechange'); 27830 } 27831 27832 pendingTimelineChange({ 27833 type, 27834 from, 27835 to 27836 }) { 27837 if (typeof from === 'number' && typeof to === 'number') { 27838 this.pendingTimelineChanges_[type] = { 27839 type, 27840 from, 27841 to 27842 }; 27843 this.trigger('pendingtimelinechange'); 27844 } 27845 27846 return this.pendingTimelineChanges_[type]; 27847 } 27848 27849 lastTimelineChange({ 27850 type, 27851 from, 27852 to 27853 }) { 27854 if (typeof from === 'number' && typeof to === 'number') { 27855 this.lastTimelineChanges_[type] = { 27856 type, 27857 from, 27858 to 27859 }; 27860 delete this.pendingTimelineChanges_[type]; 27861 this.trigger('timelinechange'); 27862 } 27863 27864 return this.lastTimelineChanges_[type]; 27865 } 27866 27867 dispose() { 27868 this.trigger('dispose'); 27869 this.pendingTimelineChanges_ = {}; 27870 this.lastTimelineChanges_ = {}; 27871 this.off(); 27872 } 27873 27874 } 27875 27876 /* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/decrypter-worker.js */ 27877 const workerCode = transform(getWorkerString(function () { 27878 /** 27879 * @file stream.js 27880 */ 27881 27882 /** 27883 * A lightweight readable stream implemention that handles event dispatching. 27884 * 27885 * @class Stream 27886 */ 27887 27888 var Stream = /*#__PURE__*/function () { 27889 function Stream() { 27890 this.listeners = {}; 27891 } 27892 /** 27893 * Add a listener for a specified event type. 27894 * 27895 * @param {string} type the event name 27896 * @param {Function} listener the callback to be invoked when an event of 27897 * the specified type occurs 27898 */ 27899 27900 27901 var _proto = Stream.prototype; 27902 27903 _proto.on = function on(type, listener) { 27904 if (!this.listeners[type]) { 27905 this.listeners[type] = []; 27906 } 27907 27908 this.listeners[type].push(listener); 27909 } 27910 /** 27911 * Remove a listener for a specified event type. 27912 * 27913 * @param {string} type the event name 27914 * @param {Function} listener a function previously registered for this 27915 * type of event through `on` 27916 * @return {boolean} if we could turn it off or not 27917 */ 27918 ; 27919 27920 _proto.off = function off(type, listener) { 27921 if (!this.listeners[type]) { 27922 return false;
vendor: 2,825 bytes, lines 27923-28012
27923 } 27924 27925 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 27926 // In Video.js we slice listener functions 27927 // on trigger so that it does not mess up the order 27928 // while we loop through. 27929 // 27930 // Here we slice on off so that the loop in trigger 27931 // can continue using it's old reference to loop without 27932 // messing up the order. 27933 27934 this.listeners[type] = this.listeners[type].slice(0); 27935 this.listeners[type].splice(index, 1); 27936 return index > -1; 27937 } 27938 /** 27939 * Trigger an event of the specified type on this stream. Any additional 27940 * arguments to this function are passed as parameters to event listeners. 27941 * 27942 * @param {string} type the event name 27943 */ 27944 ; 27945 27946 _proto.trigger = function trigger(type) { 27947 var callbacks = this.listeners[type]; 27948 27949 if (!callbacks) { 27950 return; 27951 } // Slicing the arguments on every invocation of this method 27952 // can add a significant amount of overhead. Avoid the 27953 // intermediate object creation for the common case of a 27954 // single callback argument 27955 27956 27957 if (arguments.length === 2) { 27958 var length = callbacks.length; 27959 27960 for (var i = 0; i < length; ++i) { 27961 callbacks[i].call(this, arguments[1]); 27962 } 27963 } else { 27964 var args = Array.prototype.slice.call(arguments, 1); 27965 var _length = callbacks.length; 27966 27967 for (var _i = 0; _i < _length; ++_i) { 27968 callbacks[_i].apply(this, args); 27969 } 27970 } 27971 } 27972 /** 27973 * Destroys the stream and cleans up. 27974 */ 27975 ; 27976 27977 _proto.dispose = function dispose() { 27978 this.listeners = {}; 27979 } 27980 /** 27981 * Forwards all `data` events on this stream to the destination stream. The 27982 * destination stream should provide a method `push` to receive the data 27983 * events as they arrive. 27984 * 27985 * @param {Stream} destination the stream that will receive all `data` events 27986 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 27987 */ 27988 ; 27989 27990 _proto.pipe = function pipe(destination) { 27991 this.on('data', function (data) { 27992 destination.push(data); 27993 }); 27994 }; 27995 27996 return Stream; 27997 }(); 27998 /*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */ 27999 28000 /** 28001 * Returns the subarray of a Uint8Array without PKCS#7 padding. 28002 * 28003 * @param padded {Uint8Array} unencrypted bytes that have been padded 28004 * @return {Uint8Array} the unpadded bytes 28005 * @see http://tools.ietf.org/html/rfc5652 28006 */ 28007 28008 28009 function unpad(padded) { 28010 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 28011 } 28012 /*! @name aes-decrypter @version 4.0.1
vendor: 17,514 bytes, lines 28012-28529
28012 @license Apache-2.0 */ 28013 28014 /** 28015 * @file aes.js 28016 * 28017 * This file contains an adaptation of the AES decryption algorithm 28018 * from the Standford Javascript Cryptography Library. That work is 28019 * covered by the following copyright and permissions notice: 28020 * 28021 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 28022 * All rights reserved. 28023 * 28024 * Redistribution and use in source and binary forms, with or without 28025 * modification, are permitted provided that the following conditions are 28026 * met: 28027 * 28028 * 1. Redistributions of source code must retain the above copyright 28029 * notice, this list of conditions and the following disclaimer. 28030 * 28031 * 2. Redistributions in binary form must reproduce the above 28032 * copyright notice, this list of conditions and the following 28033 * disclaimer in the documentation and/or other materials provided 28034 * with the distribution. 28035 * 28036 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 28037 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28038 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28039 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 28040 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 28041 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 28042 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 28043 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28044 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 28045 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 28046 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28047 * 28048 * The views and conclusions contained in the software and documentation 28049 * are those of the authors and should not be interpreted as representing 28050 * official policies, either expressed or implied, of the authors. 28051 */ 28052 28053 /** 28054 * Expand the S-box tables. 28055 * 28056 * @private 28057 */ 28058 28059 28060 const precompute = function () { 28061 const tables = [[[], [], [], [], []], [[], [], [], [], []]]; 28062 const encTable = tables[0]; 28063 const decTable = tables[1]; 28064 const sbox = encTable[4]; 28065 const sboxInv = decTable[4]; 28066 let i; 28067 let x; 28068 let xInv; 28069 const d = []; 28070 const th = []; 28071 let x2; 28072 let x4; 28073 let x8; 28074 let s; 28075 let tEnc; 28076 let tDec; // Compute double and third tables 28077 28078 for (i = 0; i < 256; i++) { 28079 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 28080 } 28081 28082 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 28083 // Compute sbox 28084 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 28085 s = s >> 8 ^ s & 255 ^ 99; 28086 sbox[x] = s; 28087 sboxInv[s] = x; // Compute MixColumns 28088 28089 x8 = d[x4 = d[x2 = d[x]]]; 28090 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 28091 tEnc = d[s] * 0x101 ^ s * 0x1010100; 28092 28093 for (i = 0; i < 4; i++) { 28094 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 28095 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 28096 } 28097 } // Compactify. Considerable speedup on Firefox. 28098 28099 28100 for (i = 0; i < 5; i++) { 28101 encTable[i] = encTable[i].slice(0); 28102 decTable[i] = decTable[i].slice(0); 28103 } 28104 28105 return tables; 28106 }; 28107 28108 let aesTables = null; 28109 /** 28110 * Schedule out an AES key for both encryption and decryption. This 28111 * is a low-level class. Use a cipher mode to do bulk encryption. 28112 * 28113 * @class AES 28114 * @param key {Array} The key as an array of 4, 6 or 8 words. 28115 */ 28116 28117 class AES { 28118 constructor(key) { 28119 /** 28120 * The expanded S-box and inverse S-box tables. These will be computed 28121 * on the client so that we don't have to send them down the wire. 28122 * 28123 * There are two tables, _tables[0] is for encryption and 28124 * _tables[1] is for decryption. 28125 * 28126 * The first 4 sub-tables are the expanded S-box with MixColumns. The 28127 * last (_tables[01][4]) is the S-box itself. 28128 * 28129 * @private 28130 */ 28131 // if we have yet to precompute the S-box tables 28132 // do so now 28133 if (!aesTables) { 28134 aesTables = precompute(); 28135 } // then make a copy of that object for use 28136 28137 28138 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()]]; 28139 let i; 28140 let j; 28141 let tmp; 28142 const sbox = this._tables[0][4]; 28143 const decTable = this._tables[1]; 28144 const keyLen = key.length; 28145 let rcon = 1; 28146 28147 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 28148 throw new Error('Invalid aes key size'); 28149 } 28150 28151 const encKey = key.slice(0); 28152 const decKey = []; 28153 this._key = [encKey, decKey]; // schedule encryption keys 28154 28155 for (i = keyLen; i < 4 * keyLen + 28; i++) { 28156 tmp = encKey[i - 1]; // apply sbox 28157 28158 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 28159 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon 28160 28161 if (i % keyLen === 0) { 28162 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 28163 rcon = rcon << 1 ^ (rcon >> 7) * 283; 28164 } 28165 } 28166 28167 encKey[i] = encKey[i - keyLen] ^ tmp; 28168 } // schedule decryption keys 28169 28170 28171 for (j = 0; i; j++, i--) { 28172 tmp = encKey[j & 3 ? i : i - 4]; 28173 28174 if (i <= 4 || j < 4) { 28175 decKey[j] = tmp; 28176 } else { 28177 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 28178 } 28179 } 28180 } 28181 /** 28182 * Decrypt 16 bytes, specified as four 32-bit words. 28183 * 28184 * @param {number} encrypted0 the first word to decrypt 28185 * @param {number} encrypted1 the second word to decrypt 28186 * @param {number} encrypted2 the third word to decrypt 28187 * @param {number} encrypted3 the fourth word to decrypt 28188 * @param {Int32Array} out the array to write the decrypted words 28189 * into 28190 * @param {number} offset the offset into the output array to start 28191 * writing results 28192 * @return {Array} The plaintext. 28193 */ 28194 28195 28196 decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 28197 const key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data 28198 28199 let a = encrypted0 ^ key[0]; 28200 let b = encrypted3 ^ key[1]; 28201 let c = encrypted2 ^ key[2]; 28202 let d = encrypted1 ^ key[3]; 28203 let a2; 28204 let b2; 28205 let c2; // key.length === 2 ? 28206 28207 const nInnerRounds = key.length / 4 - 2; 28208 let i; 28209 let kIndex = 4; 28210 const table = this._tables[1]; // load up the tables 28211 28212 const table0 = table[0]; 28213 const table1 = table[1]; 28214 const table2 = table[2]; 28215 const table3 = table[3]; 28216 const sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. 28217 28218 for (i = 0; i < nInnerRounds; i++) { 28219 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 28220 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 28221 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 28222 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 28223 kIndex += 4; 28224 a = a2; 28225 b = b2; 28226 c = c2; 28227 } // Last round. 28228 28229 28230 for (i = 0; i < 4; i++) { 28231 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 28232 a2 = a; 28233 a = b; 28234 b = c; 28235 c = d; 28236 d = a2; 28237 } 28238 } 28239 28240 } 28241 /** 28242 * @file async-stream.js 28243 */ 28244 28245 /** 28246 * A wrapper around the Stream class to use setTimeout 28247 * and run stream "jobs" Asynchronously 28248 * 28249 * @class AsyncStream 28250 * @extends Stream 28251 */ 28252 28253 28254 class AsyncStream extends Stream { 28255 constructor() { 28256 super(Stream); 28257 this.jobs = []; 28258 this.delay = 1; 28259 this.timeout_ = null; 28260 } 28261 /** 28262 * process an async job 28263 * 28264 * @private 28265 */ 28266 28267 28268 processJob_() { 28269 this.jobs.shift()(); 28270 28271 if (this.jobs.length) { 28272 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 28273 } else { 28274 this.timeout_ = null; 28275 } 28276 } 28277 /** 28278 * push a job into the stream 28279 * 28280 * @param {Function} job the job to push into the stream 28281 */ 28282 28283 28284 push(job) { 28285 this.jobs.push(job); 28286 28287 if (!this.timeout_) { 28288 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 28289 } 28290 } 28291 28292 } 28293 /** 28294 * @file decrypter.js 28295 * 28296 * An asynchronous implementation of AES-128 CBC decryption with 28297 * PKCS#7 padding. 28298 */ 28299 28300 /** 28301 * Convert network-order (big-endian) bytes into their little-endian 28302 * representation. 28303 */ 28304 28305 28306 const ntoh = function (word) { 28307 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 28308 }; 28309 /** 28310 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 28311 * 28312 * @param {Uint8Array} encrypted the encrypted bytes 28313 * @param {Uint32Array} key the bytes of the decryption key 28314 * @param {Uint32Array} initVector the initialization vector (IV) to 28315 * use for the first round of CBC. 28316 * @return {Uint8Array} the decrypted bytes 28317 * 28318 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 28319 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 28320 * @see https://tools.ietf.org/html/rfc2315 28321 */ 28322 28323 28324 const decrypt = function (encrypted, key, initVector) { 28325 // word-level access to the encrypted bytes 28326 const encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 28327 const decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output 28328 28329 const decrypted = new Uint8Array(encrypted.byteLength); 28330 const decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and 28331 // decrypted data 28332 28333 let init0; 28334 let init1; 28335 let init2; 28336 let init3; 28337 let encrypted0; 28338 let encrypted1; 28339 let encrypted2; 28340 let encrypted3; // iteration variable 28341 28342 let wordIx; // pull out the words of the IV to ensure we don't modify the 28343 // passed-in reference and easier access 28344 28345 init0 = initVector[0]; 28346 init1 = initVector[1]; 28347 init2 = initVector[2]; 28348 init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) 28349 // to each decrypted block 28350 28351 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 28352 // convert big-endian (network order) words into little-endian 28353 // (javascript order) 28354 encrypted0 = ntoh(encrypted32[wordIx]); 28355 encrypted1 = ntoh(encrypted32[wordIx + 1]); 28356 encrypted2 = ntoh(encrypted32[wordIx + 2]); 28357 encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block 28358 28359 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the 28360 // plaintext 28361 28362 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 28363 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 28364 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 28365 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round 28366 28367 init0 = encrypted0; 28368 init1 = encrypted1; 28369 init2 = encrypted2; 28370 init3 = encrypted3; 28371 } 28372 28373 return decrypted; 28374 }; 28375 /** 28376 * The `Decrypter` class that manages decryption of AES 28377 * data through `AsyncStream` objects and the `decrypt` 28378 * function 28379 * 28380 * @param {Uint8Array} encrypted the encrypted bytes 28381 * @param {Uint32Array} key the bytes of the decryption key 28382 * @param {Uint32Array} initVector the initialization vector (IV) to 28383 * @param {Function} done the function to run when done 28384 * @class Decrypter 28385 */ 28386 28387 28388 class Decrypter { 28389 constructor(encrypted, key, initVector, done) { 28390 const step = Decrypter.STEP; 28391 const encrypted32 = new Int32Array(encrypted.buffer); 28392 const decrypted = new Uint8Array(encrypted.byteLength); 28393 let i = 0; 28394 this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously 28395 28396 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 28397 28398 for (i = step; i < encrypted32.length; i += step) { 28399 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 28400 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 28401 } // invoke the done() callback when everything is finished 28402 28403 28404 this.asyncStream_.push(function () { 28405 // remove pkcs#7 padding from the decrypted bytes 28406 done(null, unpad(decrypted)); 28407 }); 28408 } 28409 /** 28410 * a getter for step the maximum number of bytes to process at one time 28411 * 28412 * @return {number} the value of step 32000 28413 */ 28414 28415 28416 static get STEP() { 28417 // 4 * 8000; 28418 return 32000; 28419 } 28420 /** 28421 * @private 28422 */ 28423 28424 28425 decryptChunk_(encrypted, key, initVector, decrypted) { 28426 return function () { 28427 const bytes = decrypt(encrypted, key, initVector); 28428 decrypted.set(bytes, encrypted.byteOffset); 28429 }; 28430 } 28431 28432 } 28433 28434 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 28435 var win; 28436 28437 if (typeof window !== "undefined") { 28438 win = window; 28439 } else if (typeof commonjsGlobal !== "undefined") { 28440 win = commonjsGlobal; 28441 } else if (typeof self !== "undefined") { 28442 win = self; 28443 } else { 28444 win = {}; 28445 } 28446 28447 var window_1 = win; 28448 28449 var isArrayBufferView = function isArrayBufferView(obj) { 28450 if (ArrayBuffer.isView === 'function') { 28451 return ArrayBuffer.isView(obj); 28452 } 28453 28454 return obj && obj.buffer instanceof ArrayBuffer; 28455 }; 28456 28457 var BigInt = window_1.BigInt || Number; 28458 [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')]; 28459 28460 (function () { 28461 var a = new Uint16Array([0xFFCC]); 28462 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 28463 28464 if (b[0] === 0xFF) { 28465 return 'big'; 28466 } 28467 28468 if (b[0] === 0xCC) { 28469 return 'little'; 28470 } 28471 28472 return 'unknown'; 28473 })(); 28474 /** 28475 * Creates an object for sending to a web worker modifying properties that are TypedArrays 28476 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 28477 * 28478 * @param {Object} message 28479 * Object of properties and values to send to the web worker 28480 * @return {Object} 28481 * Modified message with TypedArray values expanded 28482 * @function createTransferableMessage 28483 */ 28484 28485 28486 const createTransferableMessage = function (message) { 28487 const transferable = {}; 28488 Object.keys(message).forEach(key => { 28489 const value = message[key]; 28490 28491 if (isArrayBufferView(value)) { 28492 transferable[key] = { 28493 bytes: value.buffer, 28494 byteOffset: value.byteOffset, 28495 byteLength: value.byteLength 28496 }; 28497 } else { 28498 transferable[key] = value; 28499 } 28500 }); 28501 return transferable; 28502 }; 28503 /* global self */ 28504 28505 /** 28506 * Our web worker interface so that things can talk to aes-decrypter 28507 * that will be running in a web worker. the scope is passed to this by 28508 * webworkify. 28509 */ 28510 28511 28512 self.onmessage = function (event) { 28513 const data = event.data; 28514 const encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength); 28515 const key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 28516 const iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 28517 /* eslint-disable no-new, handle-callback-err */ 28518 28519 new Decrypter(encrypted, key, iv, function (err, bytes) { 28520 self.postMessage(createTransferableMessage({ 28521 source: data.source, 28522 decrypted: bytes 28523 }), [bytes.buffer]); 28524 }); 28525 /* eslint-enable */ 28526 }; 28527 })); 28528 var Decrypter = factory(workerCode); 28529 /* rollup-plugin-worker-factory end for worker!/
28529home/runner/work/http-streaming
vendor: 36,806 bytes, lines 28529-29628
28529/http-streaming/src/decrypter-worker.js */ 28530 28531 /** 28532 * Convert the properties of an HLS track into an audioTrackKind. 28533 * 28534 * @private 28535 */ 28536 28537 const audioTrackKind_ = properties => { 28538 let kind = properties.default ? 'main' : 'alternative'; 28539 28540 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) { 28541 kind = 'main-desc'; 28542 } 28543 28544 return kind; 28545 }; 28546 /** 28547 * Pause provided segment loader and playlist loader if active 28548 * 28549 * @param {SegmentLoader} segmentLoader 28550 * SegmentLoader to pause 28551 * @param {Object} mediaType 28552 * Active media type 28553 * @function stopLoaders 28554 */ 28555 28556 28557 const stopLoaders = (segmentLoader, mediaType) => { 28558 segmentLoader.abort(); 28559 segmentLoader.pause(); 28560 28561 if (mediaType && mediaType.activePlaylistLoader) { 28562 mediaType.activePlaylistLoader.pause(); 28563 mediaType.activePlaylistLoader = null; 28564 } 28565 }; 28566 /** 28567 * Start loading provided segment loader and playlist loader 28568 * 28569 * @param {PlaylistLoader} playlistLoader 28570 * PlaylistLoader to start loading 28571 * @param {Object} mediaType 28572 * Active media type 28573 * @function startLoaders 28574 */ 28575 28576 const startLoaders = (playlistLoader, mediaType) => { 28577 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 28578 // playlist loader 28579 mediaType.activePlaylistLoader = playlistLoader; 28580 playlistLoader.load(); 28581 }; 28582 /** 28583 * Returns a function to be called when the media group changes. It performs a 28584 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 28585 * change of group is merely a rendition switch of the same content at another encoding, 28586 * rather than a change of content, such as switching audio from English to Spanish. 28587 * 28588 * @param {string} type 28589 * MediaGroup type 28590 * @param {Object} settings 28591 * Object containing required information for media groups 28592 * @return {Function} 28593 * Handler for a non-destructive resync of SegmentLoader when the active media 28594 * group changes. 28595 * @function onGroupChanged 28596 */ 28597 28598 const onGroupChanged = (type, settings) => () => { 28599 const { 28600 segmentLoaders: { 28601 [type]: segmentLoader, 28602 main: mainSegmentLoader 28603 }, 28604 mediaTypes: { 28605 [type]: mediaType 28606 } 28607 } = settings; 28608 const activeTrack = mediaType.activeTrack(); 28609 const activeGroup = mediaType.getActiveGroup(); 28610 const previousActiveLoader = mediaType.activePlaylistLoader; 28611 const lastGroup = mediaType.lastGroup_; // the group did not change do nothing 28612 28613 if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) { 28614 return; 28615 } 28616 28617 mediaType.lastGroup_ = activeGroup; 28618 mediaType.lastTrack_ = activeTrack; 28619 stopLoaders(segmentLoader, mediaType); 28620 28621 if (!activeGroup || activeGroup.isMainPlaylist) { 28622 // there is no group active or active group is a main playlist and won't change 28623 return; 28624 } 28625 28626 if (!activeGroup.playlistLoader) { 28627 if (previousActiveLoader) { 28628 // The previous group had a playlist loader but the new active group does not 28629 // this means we are switching from demuxed to muxed audio. In this case we want to 28630 // do a destructive reset of the main segment loader and not restart the audio 28631 // loaders. 28632 mainSegmentLoader.resetEverything(); 28633 } 28634 28635 return; 28636 } // Non-destructive resync 28637 28638 28639 segmentLoader.resyncLoader(); 28640 startLoaders(activeGroup.playlistLoader, mediaType); 28641 }; 28642 const onGroupChanging = (type, settings) => () => { 28643 const { 28644 segmentLoaders: { 28645 [type]: segmentLoader 28646 }, 28647 mediaTypes: { 28648 [type]: mediaType 28649 } 28650 } = settings; 28651 mediaType.lastGroup_ = null; 28652 segmentLoader.abort(); 28653 segmentLoader.pause(); 28654 }; 28655 /** 28656 * Returns a function to be called when the media track changes. It performs a 28657 * destructive reset of the SegmentLoader to ensure we start loading as close to 28658 * currentTime as possible. 28659 * 28660 * @param {string} type 28661 * MediaGroup type 28662 * @param {Object} settings 28663 * Object containing required information for media groups 28664 * @return {Function} 28665 * Handler for a destructive reset of SegmentLoader when the active media 28666 * track changes. 28667 * @function onTrackChanged 28668 */ 28669 28670 const onTrackChanged = (type, settings) => () => { 28671 const { 28672 mainPlaylistLoader, 28673 segmentLoaders: { 28674 [type]: segmentLoader, 28675 main: mainSegmentLoader 28676 }, 28677 mediaTypes: { 28678 [type]: mediaType 28679 } 28680 } = settings; 28681 const activeTrack = mediaType.activeTrack(); 28682 const activeGroup = mediaType.getActiveGroup(); 28683 const previousActiveLoader = mediaType.activePlaylistLoader; 28684 const lastTrack = mediaType.lastTrack_; // track did not change, do nothing 28685 28686 if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) { 28687 return; 28688 } 28689 28690 mediaType.lastGroup_ = activeGroup; 28691 mediaType.lastTrack_ = activeTrack; 28692 stopLoaders(segmentLoader, mediaType); 28693 28694 if (!activeGroup) { 28695 // there is no group active so we do not want to restart loaders 28696 return; 28697 } 28698 28699 if (activeGroup.isMainPlaylist) { 28700 // track did not change, do nothing 28701 if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) { 28702 return; 28703 } 28704 28705 const pc = settings.vhs.playlistController_; 28706 const newPlaylist = pc.selectPlaylist(); // media will not change do nothing 28707 28708 if (pc.media() === newPlaylist) { 28709 return; 28710 } 28711 28712 mediaType.logger_(`track change. Switching main audio from ${lastTrack.id} to ${activeTrack.id}`); 28713 mainPlaylistLoader.pause(); 28714 mainSegmentLoader.resetEverything(); 28715 pc.fastQualityChange_(newPlaylist); 28716 return; 28717 } 28718 28719 if (type === 'AUDIO') { 28720 if (!activeGroup.playlistLoader) { 28721 // when switching from demuxed audio/video to muxed audio/video (noted by no 28722 // playlist loader for the audio group), we want to do a destructive reset of the 28723 // main segment loader and not restart the audio loaders 28724 mainSegmentLoader.setAudio(true); // don't have to worry about disabling the audio of the audio segment loader since 28725 // it should be stopped 28726 28727 mainSegmentLoader.resetEverything(); 28728 return; 28729 } // although the segment loader is an audio segment loader, call the setAudio 28730 // function to ensure it is prepared to re-append the init segment (or handle other 28731 // config changes) 28732 28733 28734 segmentLoader.setAudio(true); 28735 mainSegmentLoader.setAudio(false); 28736 } 28737 28738 if (previousActiveLoader === activeGroup.playlistLoader) { 28739 // Nothing has actually changed. This can happen because track change events can fire 28740 // multiple times for a "single" change. One for enabling the new active track, and 28741 // one for disabling the track that was active 28742 startLoaders(activeGroup.playlistLoader, mediaType); 28743 return; 28744 } 28745 28746 if (segmentLoader.track) { 28747 // For WebVTT, set the new text track in the segmentloader 28748 segmentLoader.track(activeTrack); 28749 } // destructive reset 28750 28751 28752 segmentLoader.resetEverything(); 28753 startLoaders(activeGroup.playlistLoader, mediaType); 28754 }; 28755 const onError = { 28756 /** 28757 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 28758 * an error. 28759 * 28760 * @param {string} type 28761 * MediaGroup type 28762 * @param {Object} settings 28763 * Object containing required information for media groups 28764 * @return {Function} 28765 * Error handler. Logs warning (or error if the playlist is excluded) to 28766 * console and switches back to default audio track. 28767 * @function onError.AUDIO 28768 */ 28769 AUDIO: (type, settings) => () => { 28770 const { 28771 mediaTypes: { 28772 [type]: mediaType 28773 }, 28774 excludePlaylist 28775 } = settings; // switch back to default audio track 28776 28777 const activeTrack = mediaType.activeTrack(); 28778 const activeGroup = mediaType.activeGroup(); 28779 const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id; 28780 const defaultTrack = mediaType.tracks[id]; 28781 28782 if (activeTrack === defaultTrack) { 28783 // Default track encountered an error. All we can do now is exclude the current 28784 // rendition and hope another will switch audio groups 28785 excludePlaylist({ 28786 error: { 28787 message: 'Problem encountered loading the default audio track.' 28788 } 28789 }); 28790 return; 28791 } 28792 28793 videojs__default["default"].log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.'); 28794 28795 for (const trackId in mediaType.tracks) { 28796 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 28797 } 28798 28799 mediaType.onTrackChanged(); 28800 }, 28801 28802 /** 28803 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 28804 * an error. 28805 * 28806 * @param {string} type 28807 * MediaGroup type 28808 * @param {Object} settings 28809 * Object containing required information for media groups 28810 * @return {Function} 28811 * Error handler. Logs warning to console and disables the active subtitle track 28812 * @function onError.SUBTITLES 28813 */ 28814 SUBTITLES: (type, settings) => () => { 28815 const { 28816 mediaTypes: { 28817 [type]: mediaType 28818 } 28819 } = settings; 28820 videojs__default["default"].log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.'); 28821 const track = mediaType.activeTrack(); 28822 28823 if (track) { 28824 track.mode = 'disabled'; 28825 } 28826 28827 mediaType.onTrackChanged(); 28828 } 28829 }; 28830 const setupListeners = { 28831 /** 28832 * Setup event listeners for audio playlist loader 28833 * 28834 * @param {string} type 28835 * MediaGroup type 28836 * @param {PlaylistLoader|null} playlistLoader 28837 * PlaylistLoader to register listeners on 28838 * @param {Object} settings 28839 * Object containing required information for media groups 28840 * @function setupListeners.AUDIO 28841 */ 28842 AUDIO: (type, playlistLoader, settings) => { 28843 if (!playlistLoader) { 28844 // no playlist loader means audio will be muxed with the video 28845 return; 28846 } 28847 28848 const { 28849 tech, 28850 requestOptions, 28851 segmentLoaders: { 28852 [type]: segmentLoader 28853 } 28854 } = settings; 28855 playlistLoader.on('loadedmetadata', () => { 28856 const media = playlistLoader.media(); 28857 segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload 28858 // permits, start downloading segments 28859 28860 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 28861 segmentLoader.load(); 28862 } 28863 }); 28864 playlistLoader.on('loadedplaylist', () => { 28865 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 28866 28867 if (!tech.paused()) { 28868 segmentLoader.load(); 28869 } 28870 }); 28871 playlistLoader.on('error', onError[type](type, settings)); 28872 }, 28873 28874 /** 28875 * Setup event listeners for subtitle playlist loader 28876 * 28877 * @param {string} type 28878 * MediaGroup type 28879 * @param {PlaylistLoader|null} playlistLoader 28880 * PlaylistLoader to register listeners on 28881 * @param {Object} settings 28882 * Object containing required information for media groups 28883 * @function setupListeners.SUBTITLES 28884 */ 28885 SUBTITLES: (type, playlistLoader, settings) => { 28886 const { 28887 tech, 28888 requestOptions, 28889 segmentLoaders: { 28890 [type]: segmentLoader 28891 }, 28892 mediaTypes: { 28893 [type]: mediaType 28894 } 28895 } = settings; 28896 playlistLoader.on('loadedmetadata', () => { 28897 const media = playlistLoader.media(); 28898 segmentLoader.playlist(media, requestOptions); 28899 segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload 28900 // permits, start downloading segments 28901 28902 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 28903 segmentLoader.load(); 28904 } 28905 }); 28906 playlistLoader.on('loadedplaylist', () => { 28907 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 28908 28909 if (!tech.paused()) { 28910 segmentLoader.load(); 28911 } 28912 }); 28913 playlistLoader.on('error', onError[type](type, settings)); 28914 } 28915 }; 28916 const initialize = { 28917 /** 28918 * Setup PlaylistLoaders and AudioTracks for the audio groups 28919 * 28920 * @param {string} type 28921 * MediaGroup type 28922 * @param {Object} settings 28923 * Object containing required information for media groups 28924 * @function initialize.AUDIO 28925 */ 28926 'AUDIO': (type, settings) => { 28927 const { 28928 vhs, 28929 sourceType, 28930 segmentLoaders: { 28931 [type]: segmentLoader 28932 }, 28933 requestOptions, 28934 main: { 28935 mediaGroups 28936 }, 28937 mediaTypes: { 28938 [type]: { 28939 groups, 28940 tracks, 28941 logger_ 28942 } 28943 }, 28944 mainPlaylistLoader 28945 } = settings; 28946 const audioOnlyMain = isAudioOnly(mainPlaylistLoader.main); // force a default if we have none 28947 28948 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 28949 mediaGroups[type] = { 28950 main: { 28951 default: { 28952 default: true 28953 } 28954 } 28955 }; 28956 28957 if (audioOnlyMain) { 28958 mediaGroups[type].main.default.playlists = mainPlaylistLoader.main.playlists; 28959 } 28960 } 28961 28962 for (const groupId in mediaGroups[type]) { 28963 if (!groups[groupId]) { 28964 groups[groupId] = []; 28965 } 28966 28967 for (const variantLabel in mediaGroups[type][groupId]) { 28968 let properties = mediaGroups[type][groupId][variantLabel]; 28969 let playlistLoader; 28970 28971 if (audioOnlyMain) { 28972 logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a main playlist`); 28973 properties.isMainPlaylist = true; 28974 playlistLoader = null; // if vhs-json was provided as the source, and the media playlist was resolved, 28975 // use the resolved media playlist object 28976 } else if (sourceType === 'vhs-json' && properties.playlists) { 28977 playlistLoader = new PlaylistLoader(properties.playlists[0], vhs, requestOptions); 28978 } else if (properties.resolvedUri) { 28979 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); // TODO: dash isn't the only type with properties.playlists 28980 // should we even have properties.playlists in this check. 28981 } else if (properties.playlists && sourceType === 'dash') { 28982 playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader); 28983 } else { 28984 // no resolvedUri means the audio is muxed with the video when using this 28985 // audio track 28986 playlistLoader = null; 28987 } 28988 28989 properties = merge$1({ 28990 id: variantLabel, 28991 playlistLoader 28992 }, properties); 28993 setupListeners[type](type, properties.playlistLoader, settings); 28994 groups[groupId].push(properties); 28995 28996 if (typeof tracks[variantLabel] === 'undefined') { 28997 const track = new videojs__default["default"].AudioTrack({ 28998 id: variantLabel, 28999 kind: audioTrackKind_(properties), 29000 enabled: false, 29001 language: properties.language, 29002 default: properties.default, 29003 label: variantLabel 29004 }); 29005 tracks[variantLabel] = track; 29006 } 29007 } 29008 } // setup single error event handler for the segment loader 29009 29010 29011 segmentLoader.on('error', onError[type](type, settings)); 29012 }, 29013 29014 /** 29015 * Setup PlaylistLoaders and TextTracks for the subtitle groups 29016 * 29017 * @param {string} type 29018 * MediaGroup type 29019 * @param {Object} settings 29020 * Object containing required information for media groups 29021 * @function initialize.SUBTITLES 29022 */ 29023 'SUBTITLES': (type, settings) => { 29024 const { 29025 tech, 29026 vhs, 29027 sourceType, 29028 segmentLoaders: { 29029 [type]: segmentLoader 29030 }, 29031 requestOptions, 29032 main: { 29033 mediaGroups 29034 }, 29035 mediaTypes: { 29036 [type]: { 29037 groups, 29038 tracks 29039 } 29040 }, 29041 mainPlaylistLoader 29042 } = settings; 29043 29044 for (const groupId in mediaGroups[type]) { 29045 if (!groups[groupId]) { 29046 groups[groupId] = []; 29047 } 29048 29049 for (const variantLabel in mediaGroups[type][groupId]) { 29050 if (!vhs.options_.useForcedSubtitles && mediaGroups[type][groupId][variantLabel].forced) { 29051 // Subtitle playlists with the forced attribute are not selectable in Safari. 29052 // According to Apple's HLS Authoring Specification: 29053 // If content has forced subtitles and regular subtitles in a given language, 29054 // the regular subtitles track in that language MUST contain both the forced 29055 // subtitles and the regular subtitles for that language. 29056 // Because of this requirement and that Safari does not add forced subtitles, 29057 // forced subtitles are skipped here to maintain consistent experience across 29058 // all platforms 29059 continue; 29060 } 29061 29062 let properties = mediaGroups[type][groupId][variantLabel]; 29063 let playlistLoader; 29064 29065 if (sourceType === 'hls') { 29066 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); 29067 } else if (sourceType === 'dash') { 29068 const playlists = properties.playlists.filter(p => p.excludeUntil !== Infinity); 29069 29070 if (!playlists.length) { 29071 return; 29072 } 29073 29074 playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader); 29075 } else if (sourceType === 'vhs-json') { 29076 playlistLoader = new PlaylistLoader( // if the vhs-json object included the media playlist, use the media playlist 29077 // as provided, otherwise use the resolved URI to load the playlist 29078 properties.playlists ? properties.playlists[0] : properties.resolvedUri, vhs, requestOptions); 29079 } 29080 29081 properties = merge$1({ 29082 id: variantLabel, 29083 playlistLoader 29084 }, properties); 29085 setupListeners[type](type, properties.playlistLoader, settings); 29086 groups[groupId].push(properties); 29087 29088 if (typeof tracks[variantLabel] === 'undefined') { 29089 const track = tech.addRemoteTextTrack({ 29090 id: variantLabel, 29091 kind: 'subtitles', 29092 default: properties.default && properties.autoselect, 29093 language: properties.language, 29094 label: variantLabel 29095 }, false).track; 29096 tracks[variantLabel] = track; 29097 } 29098 } 29099 } // setup single error event handler for the segment loader 29100 29101 29102 segmentLoader.on('error', onError[type](type, settings)); 29103 }, 29104 29105 /** 29106 * Setup TextTracks for the closed-caption groups 29107 * 29108 * @param {String} type 29109 * MediaGroup type 29110 * @param {Object} settings 29111 * Object containing required information for media groups 29112 * @function initialize['CLOSED-CAPTIONS'] 29113 */ 29114 'CLOSED-CAPTIONS': (type, settings) => { 29115 const { 29116 tech, 29117 main: { 29118 mediaGroups 29119 }, 29120 mediaTypes: { 29121 [type]: { 29122 groups, 29123 tracks 29124 } 29125 } 29126 } = settings; 29127 29128 for (const groupId in mediaGroups[type]) { 29129 if (!groups[groupId]) { 29130 groups[groupId] = []; 29131 } 29132 29133 for (const variantLabel in mediaGroups[type][groupId]) { 29134 const properties = mediaGroups[type][groupId][variantLabel]; // Look for either 608 (CCn) or 708 (SERVICEn) caption services 29135 29136 if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) { 29137 continue; 29138 } 29139 29140 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {}; 29141 let newProps = { 29142 label: variantLabel, 29143 language: properties.language, 29144 instreamId: properties.instreamId, 29145 default: properties.default && properties.autoselect 29146 }; 29147 29148 if (captionServices[newProps.instreamId]) { 29149 newProps = merge$1(newProps, captionServices[newProps.instreamId]); 29150 } 29151 29152 if (newProps.default === undefined) { 29153 delete newProps.default; 29154 } // No PlaylistLoader is required for Closed-Captions because the captions are 29155 // embedded within the video stream 29156 29157 29158 groups[groupId].push(merge$1({ 29159 id: variantLabel 29160 }, properties)); 29161 29162 if (typeof tracks[variantLabel] === 'undefined') { 29163 const track = tech.addRemoteTextTrack({ 29164 id: newProps.instreamId, 29165 kind: 'captions', 29166 default: newProps.default, 29167 language: newProps.language, 29168 label: newProps.label 29169 }, false).track; 29170 tracks[variantLabel] = track; 29171 } 29172 } 29173 } 29174 } 29175 }; 29176 29177 const groupMatch = (list, media) => { 29178 for (let i = 0; i < list.length; i++) { 29179 if (playlistMatch(media, list[i])) { 29180 return true; 29181 } 29182 29183 if (list[i].playlists && groupMatch(list[i].playlists, media)) { 29184 return true; 29185 } 29186 } 29187 29188 return false; 29189 }; 29190 /** 29191 * Returns a function used to get the active group of the provided type 29192 * 29193 * @param {string} type 29194 * MediaGroup type 29195 * @param {Object} settings 29196 * Object containing required information for media groups 29197 * @return {Function} 29198 * Function that returns the active media group for the provided type. Takes an 29199 * optional parameter {TextTrack} track. If no track is provided, a list of all 29200 * variants in the group, otherwise the variant corresponding to the provided 29201 * track is returned. 29202 * @function activeGroup 29203 */ 29204 29205 29206 const activeGroup = (type, settings) => track => { 29207 const { 29208 mainPlaylistLoader, 29209 mediaTypes: { 29210 [type]: { 29211 groups 29212 } 29213 } 29214 } = settings; 29215 const media = mainPlaylistLoader.media(); 29216 29217 if (!media) { 29218 return null; 29219 } 29220 29221 let variants = null; // set to variants to main media active group 29222 29223 if (media.attributes[type]) { 29224 variants = groups[media.attributes[type]]; 29225 } 29226 29227 const groupKeys = Object.keys(groups); 29228 29229 if (!variants) { 29230 // find the mainPlaylistLoader media 29231 // that is in a media group if we are dealing 29232 // with audio only 29233 if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.main)) { 29234 for (let i = 0; i < groupKeys.length; i++) { 29235 const groupPropertyList = groups[groupKeys[i]]; 29236 29237 if (groupMatch(groupPropertyList, media)) { 29238 variants = groupPropertyList; 29239 break; 29240 } 29241 } // use the main group if it exists 29242 29243 } else if (groups.main) { 29244 variants = groups.main; // only one group, use that one 29245 } else if (groupKeys.length === 1) { 29246 variants = groups[groupKeys[0]]; 29247 } 29248 } 29249 29250 if (typeof track === 'undefined') { 29251 return variants; 29252 } 29253 29254 if (track === null || !variants) { 29255 // An active track was specified so a corresponding group is expected. track === null 29256 // means no track is currently active so there is no corresponding group 29257 return null; 29258 } 29259 29260 return variants.filter(props => props.id === track.id)[0] || null; 29261 }; 29262 const activeTrack = { 29263 /** 29264 * Returns a function used to get the active track of type provided 29265 * 29266 * @param {string} type 29267 * MediaGroup type 29268 * @param {Object} settings 29269 * Object containing required information for media groups 29270 * @return {Function} 29271 * Function that returns the active media track for the provided type. Returns 29272 * null if no track is active 29273 * @function activeTrack.AUDIO 29274 */ 29275 AUDIO: (type, settings) => () => { 29276 const { 29277 mediaTypes: { 29278 [type]: { 29279 tracks 29280 } 29281 } 29282 } = settings; 29283 29284 for (const id in tracks) { 29285 if (tracks[id].enabled) { 29286 return tracks[id]; 29287 } 29288 } 29289 29290 return null; 29291 }, 29292 29293 /** 29294 * Returns a function used to get the active track of type provided 29295 * 29296 * @param {string} type 29297 * MediaGroup type 29298 * @param {Object} settings 29299 * Object containing required information for media groups 29300 * @return {Function} 29301 * Function that returns the active media track for the provided type. Returns 29302 * null if no track is active 29303 * @function activeTrack.SUBTITLES 29304 */ 29305 SUBTITLES: (type, settings) => () => { 29306 const { 29307 mediaTypes: { 29308 [type]: { 29309 tracks 29310 } 29311 } 29312 } = settings; 29313 29314 for (const id in tracks) { 29315 if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') { 29316 return tracks[id]; 29317 } 29318 } 29319 29320 return null; 29321 } 29322 }; 29323 const getActiveGroup = (type, { 29324 mediaTypes 29325 }) => () => { 29326 const activeTrack_ = mediaTypes[type].activeTrack(); 29327 29328 if (!activeTrack_) { 29329 return null; 29330 } 29331 29332 return mediaTypes[type].activeGroup(activeTrack_); 29333 }; 29334 /** 29335 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 29336 * Closed-Captions) specified in the main manifest. 29337 * 29338 * @param {Object} settings 29339 * Object containing required information for setting up the media groups 29340 * @param {Tech} settings.tech 29341 * The tech of the player 29342 * @param {Object} settings.requestOptions 29343 * XHR request options used by the segment loaders 29344 * @param {PlaylistLoader} settings.mainPlaylistLoader 29345 * PlaylistLoader for the main source 29346 * @param {VhsHandler} settings.vhs 29347 * VHS SourceHandler 29348 * @param {Object} settings.main 29349 * The parsed main manifest 29350 * @param {Object} settings.mediaTypes 29351 * Object to store the loaders, tracks, and utility methods for each media type 29352 * @param {Function} settings.excludePlaylist 29353 * Excludes the current rendition and forces a rendition switch. 29354 * @function setupMediaGroups 29355 */ 29356 29357 const setupMediaGroups = settings => { 29358 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 29359 initialize[type](type, settings); 29360 }); 29361 const { 29362 mediaTypes, 29363 mainPlaylistLoader, 29364 tech, 29365 vhs, 29366 segmentLoaders: { 29367 ['AUDIO']: audioSegmentLoader, 29368 main: mainSegmentLoader 29369 } 29370 } = settings; // setup active group and track getters and change event handlers 29371 29372 ['AUDIO', 'SUBTITLES'].forEach(type => { 29373 mediaTypes[type].activeGroup = activeGroup(type, settings); 29374 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 29375 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 29376 mediaTypes[type].onGroupChanging = onGroupChanging(type, settings); 29377 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 29378 mediaTypes[type].getActiveGroup = getActiveGroup(type, settings); 29379 }); // DO NOT enable the default subtitle or caption track. 29380 // DO enable the default audio track 29381 29382 const audioGroup = mediaTypes.AUDIO.activeGroup(); 29383 29384 if (audioGroup) { 29385 const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id; 29386 mediaTypes.AUDIO.tracks[groupId].enabled = true; 29387 mediaTypes.AUDIO.onGroupChanged(); 29388 mediaTypes.AUDIO.onTrackChanged(); 29389 const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup(); // a similar check for handling setAudio on each loader is run again each time the 29390 // track is changed, but needs to be handled here since the track may not be considered 29391 // changed on the first call to onTrackChanged 29392 29393 if (!activeAudioGroup.playlistLoader) { 29394 // either audio is muxed with video or the stream is audio only 29395 mainSegmentLoader.setAudio(true); 29396 } else { 29397 // audio is demuxed 29398 mainSegmentLoader.setAudio(false); 29399 audioSegmentLoader.setAudio(true); 29400 } 29401 } 29402 29403 mainPlaylistLoader.on('mediachange', () => { 29404 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged()); 29405 }); 29406 mainPlaylistLoader.on('mediachanging', () => { 29407 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanging()); 29408 }); // custom audio track change event handler for usage event 29409 29410 const onAudioTrackChanged = () => { 29411 mediaTypes.AUDIO.onTrackChanged(); 29412 tech.trigger({ 29413 type: 'usage', 29414 name: 'vhs-audio-change' 29415 }); 29416 }; 29417 29418 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 29419 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 29420 vhs.on('dispose', () => { 29421 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 29422 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 29423 }); // clear existing audio tracks and add the ones we just created 29424 29425 tech.clearTracks('audio'); 29426 29427 for (const id in mediaTypes.AUDIO.tracks) { 29428 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 29429 } 29430 }; 29431 /** 29432 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 29433 * media type 29434 * 29435 * @return {Object} 29436 * Object to store the loaders, tracks, and utility methods for each media type 29437 * @function createMediaTypes 29438 */ 29439 29440 const createMediaTypes = () => { 29441 const mediaTypes = {}; 29442 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 29443 mediaTypes[type] = { 29444 groups: {}, 29445 tracks: {}, 29446 activePlaylistLoader: null, 29447 activeGroup: noop, 29448 activeTrack: noop, 29449 getActiveGroup: noop, 29450 onGroupChanged: noop, 29451 onTrackChanged: noop, 29452 lastTrack_: null, 29453 logger_: logger(`MediaGroups[${type}]`) 29454 }; 29455 }); 29456 return mediaTypes; 29457 }; 29458 29459 /** 29460 * A utility class for setting properties and maintaining the state of the content steering manifest. 29461 * 29462 * Content Steering manifest format: 29463 * VERSION: number (required) currently only version 1 is supported. 29464 * TTL: number in seconds (optional) until the next content steering manifest reload. 29465 * RELOAD-URI: string (optional) uri to fetch the next content steering manifest. 29466 * SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values. 29467 * PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists. 29468 */ 29469 29470 class SteeringManifest { 29471 constructor() { 29472 this.priority_ = []; 29473 this.pathwayClones_ = new Map(); 29474 } 29475 29476 set version(number) { 29477 // Only version 1 is currently supported for both DASH and HLS. 29478 if (number === 1) { 29479 this.version_ = number; 29480 } 29481 } 29482 29483 set ttl(seconds) { 29484 // TTL = time-to-live, default = 300 seconds. 29485 this.ttl_ = seconds || 300; 29486 } 29487 29488 set reloadUri(uri) { 29489 if (uri) { 29490 // reload URI can be relative to the previous reloadUri. 29491 this.reloadUri_ = resolveUrl(this.reloadUri_, uri); 29492 } 29493 } 29494 29495 set priority(array) { 29496 // priority must be non-empty and unique values. 29497 if (array && array.length) { 29498 this.priority_ = array; 29499 } 29500 } 29501 29502 set pathwayClones(array) { 29503 // pathwayClones must be non-empty. 29504 if (array && array.length) { 29505 this.pathwayClones_ = new Map(array.map(clone => [clone.ID, clone])); 29506 } 29507 } 29508 29509 get version() { 29510 return this.version_; 29511 } 29512 29513 get ttl() { 29514 return this.ttl_; 29515 } 29516 29517 get reloadUri() { 29518 return this.reloadUri_; 29519 } 29520 29521 get priority() { 29522 return this.priority_; 29523 } 29524 29525 get pathwayClones() { 29526 return this.pathwayClones_; 29527 } 29528 29529 } 29530 /** 29531 * This class represents a content steering manifest and associated state. See both HLS and DASH specifications. 29532 * HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and 29533 * https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6. 29534 * DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 29535 * 29536 * @param {function} xhr for making a network request from the browser. 29537 * @param {function} bandwidth for fetching the current bandwidth from the main segment loader. 29538 */ 29539 29540 29541 class ContentSteeringController extends videojs__default["default"].EventTarget { 29542 constructor(xhr, bandwidth) { 29543 super(); 29544 this.currentPathway = null; 29545 this.defaultPathway = null; 29546 this.queryBeforeStart = false; 29547 this.availablePathways_ = new Set(); 29548 this.steeringManifest = new SteeringManifest(); 29549 this.proxyServerUrl_ = null; 29550 this.manifestType_ = null; 29551 this.ttlTimeout_ = null; 29552 this.request_ = null; 29553 this.currentPathwayClones = new Map(); 29554 this.nextPathwayClones = new Map(); 29555 this.excludedSteeringManifestURLs = new Set(); 29556 this.logger_ = logger('Content Steering'); 29557 this.xhr_ = xhr; 29558 this.getBandwidth_ = bandwidth; 29559 } 29560 /** 29561 * Assigns the content steering tag properties to the steering controller 29562 * 29563 * @param {string} baseUrl the baseURL from the main manifest for resolving the steering manifest url 29564 * @param {Object} steeringTag the content steering tag from the main manifest 29565 */ 29566 29567 29568 assignTagProperties(baseUrl, steeringTag) { 29569 this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH'; // serverUri is HLS serverURL is DASH 29570 29571 const steeringUri = steeringTag.serverUri || steeringTag.serverURL; 29572 29573 if (!steeringUri) { 29574 this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`); 29575 this.trigger('error'); 29576 return; 29577 } // Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case. 29578 29579 29580 if (steeringUri.startsWith('data:')) { 29581 this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1)); 29582 return; 29583 } // reloadUri is the resolution of the main manifest URL and steering URL. 29584 29585 29586 this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri); // pathwayId is HLS defaultServiceLocation is DASH 29587 29588 this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation; // currently only DASH supports the following properties on <ContentSteering> tags. 29589 29590 this.queryBeforeStart = steeringTag.queryBeforeStart; 29591 this.proxyServerUrl_ = steeringTag.proxyServerURL; // trigger a steering event if we have a pathway from the content steering tag. 29592 // this tells VHS which segment pathway to start with. 29593 // If queryBeforeStart is true we need to wait for the steering manifest response. 29594 29595 if (this.defaultPathway && !this.queryBeforeStart) { 29596 this.trigger('content-steering'); 29597 } 29598 } 29599 /** 29600 * Requests the content steering manifest and parse the response. This should only be called after 29601 * assignTagProperties was called with a content steering tag. 29602 * 29603 * @param {string} initialUri The optional uri to make the request with. 29604 * If set, the request should be made with exactly what is passed in this variable. 29605 * This scenario should only happen once on initalization. 29606 */ 29607 29608 29609 requestSteeringManifest(initial) { 29610 const reloadUri = this.steeringManifest.reloadUri; 29611 29612 if (!reloadUri) { 29613 return; 29614 } // We currently don't support passing MPD query parameters directly to the content steering URL as this requires 29615 // ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1. 29616 // This request URI accounts for manifest URIs that have been excluded. 29617 29618 29619 const uri = initial ? reloadUri : this.getRequestURI(reloadUri); // If there are no valid manifest URIs, we should stop content steering. 29620 29621 if (!uri) { 29622 this.logger_('No valid content steering manifest URIs. Stopping content steering.'); 29623 this.trigger('error'); 29624 this.dispose(); 29625 return; 29626 } 29627 29628
29628this.request_ = this.xhr_({ 29629 uri 29630 }, (error, errorInfo) => { 29631 if (error) { 29632 // If the client receives HTTP 410 Gone in response to a manifest request, 29633 // it MUST NOT issue another request for that URI for the remainder of the 29634 // playback session. It MAY continue to use the most-recently obtained set 29635 // of Pathways. 29636 if (errorInfo.status === 410) { 29637 this.logger_(`manifest request 410 ${error}.`); 29638 this.logger_(`There will be no more content steering requests to ${uri} this session.`); 29639 this.excludedSteeringManifestURLs.add(uri); 29640 return; 29641 } // If the client receives HTTP 429 Too Many Requests with a Retry-After 29642 // header in response to a manifest request, it SHOULD wait until the time 29643 // specified by the Retry-After header to reissue the request. 29644 29645 29646 if (errorInfo.status === 429) { 29647 const retrySeconds = errorInfo.responseHeaders['retry-after']; 29648 this.logger_(`manifest request 429 ${error}.`); 29649 this.logger_(`content steering will retry in ${retrySeconds} seconds.`); 29650 this.startTTLTimeout_(parseInt(retrySeconds, 10)); 29651 return; 29652 } // If the Steering Manifest cannot be loaded and parsed correctly, the 29653 // client SHOULD continue to use the previous values and attempt to reload 29654 // it after waiting for the previously-specified TTL (or 5 minutes if 29655 // none). 29656 29657 29658 this.logger_(`manifest failed to load ${error}.`); 29659 this.startTTLTimeout_(); 29660 return; 29661 } 29662 29663 const steeringManifestJson = JSON.parse(this.request_.responseText); 29664 this.assignSteeringProperties_(steeringManifestJson
vendor: 8,300 bytes, lines 29664-29924
29664); 29665 this.startTTLTimeout_(); 29666 }); 29667 } 29668 /** 29669 * Set the proxy server URL and add the steering manifest url as a URI encoded parameter. 29670 * 29671 * @param {string} steeringUrl the steering manifest url 29672 * @return the steering manifest url to a proxy server with all parameters set 29673 */ 29674 29675 29676 setProxyServerUrl_(steeringUrl) { 29677 const steeringUrlObject = new window.URL(steeringUrl); 29678 const proxyServerUrlObject = new window.URL(this.proxyServerUrl_); 29679 proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString())); 29680 return this.setSteeringParams_(proxyServerUrlObject.toString()); 29681 } 29682 /** 29683 * Decodes and parses the data uri encoded steering manifest 29684 * 29685 * @param {string} dataUri the data uri to be decoded and parsed. 29686 */ 29687 29688 29689 decodeDataUriManifest_(dataUri) { 29690 const steeringManifestJson = JSON.parse(window.atob(dataUri)); 29691 this.assignSteeringProperties_(steeringManifestJson); 29692 } 29693 /** 29694 * Set the HLS or DASH content steering manifest request query parameters. For example: 29695 * _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT> 29696 * _DASH_pathway and _DASH_throughput 29697 * 29698 * @param {string} uri to add content steering server parameters to. 29699 * @return a new uri as a string with the added steering query parameters. 29700 */ 29701 29702 29703 setSteeringParams_(url) { 29704 const urlObject = new window.URL(url); 29705 const path = this.getPathway(); 29706 const networkThroughput = this.getBandwidth_(); 29707 29708 if (path) { 29709 const pathwayKey = `_${this.manifestType_}_pathway`; 29710 urlObject.searchParams.set(pathwayKey, path); 29711 } 29712 29713 if (networkThroughput) { 29714 const throughputKey = `_${this.manifestType_}_throughput`; 29715 urlObject.searchParams.set(throughputKey, networkThroughput); 29716 } 29717 29718 return urlObject.toString(); 29719 } 29720 /** 29721 * Assigns the current steering manifest properties and to the SteeringManifest object 29722 * 29723 * @param {Object} steeringJson the raw JSON steering manifest 29724 */ 29725 29726 29727 assignSteeringProperties_(steeringJson) { 29728 this.steeringManifest.version = steeringJson.VERSION; 29729 29730 if (!this.steeringManifest.version) { 29731 this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`); 29732 this.trigger('error'); 29733 return; 29734 } 29735 29736 this.steeringManifest.ttl = steeringJson.TTL; 29737 this.steeringManifest.reloadUri = steeringJson['RELOAD-URI']; // HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional 29738 29739 this.steeringManifest.priority = steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY']; // Pathway clones to be created/updated in HLS. 29740 // See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 29741 29742 this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES']; 29743 this.nextPathwayClones = this.steeringManifest.pathwayClones; // 1. apply first pathway from the array. 29744 // 2. if first pathway doesn't exist in manifest, try next pathway. 29745 // a. if all pathways are exhausted, ignore the steering manifest priority. 29746 // 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway. 29747 // a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response, 29748 // the excluded pathway will be ignored. 29749 // See excludePathway usage in excludePlaylist(). 29750 // If there are no available pathways, we need to stop content steering. 29751 29752 if (!this.availablePathways_.size) { 29753 this.logger_('There are no available pathways for content steering. Ending content steering.'); 29754 this.trigger('error'); 29755 this.dispose(); 29756 } 29757 29758 const chooseNextPathway = pathwaysByPriority => { 29759 for (const path of pathwaysByPriority) { 29760 if (this.availablePathways_.has(path)) { 29761 return path; 29762 } 29763 } // If no pathway matches, ignore the manifest and choose the first available. 29764 29765 29766 return [...this.availablePathways_][0]; 29767 }; 29768 29769 const nextPathway = chooseNextPathway(this.steeringManifest.priority); 29770 29771 if (this.currentPathway !== nextPathway) { 29772 this.currentPathway = nextPathway; 29773 this.trigger('content-steering'); 29774 } 29775 } 29776 /** 29777 * Returns the pathway to use for steering decisions 29778 * 29779 * @return {string} returns the current pathway or the default 29780 */ 29781 29782 29783 getPathway() { 29784 return this.currentPathway || this.defaultPathway; 29785 } 29786 /** 29787 * Chooses the manifest request URI based on proxy URIs and server URLs. 29788 * Also accounts for exclusion on certain manifest URIs. 29789 * 29790 * @param {string} reloadUri the base uri before parameters 29791 * 29792 * @return {string} the final URI for the request to the manifest server. 29793 */ 29794 29795 29796 getRequestURI(reloadUri) { 29797 if (!reloadUri) { 29798 return null; 29799 } 29800 29801 const isExcluded = uri => this.excludedSteeringManifestURLs.has(uri); 29802 29803 if (this.proxyServerUrl_) { 29804 const proxyURI = this.setProxyServerUrl_(reloadUri); 29805 29806 if (!isExcluded(proxyURI)) { 29807 return proxyURI; 29808 } 29809 } 29810 29811 const steeringURI = this.setSteeringParams_(reloadUri); 29812 29813 if (!isExcluded(steeringURI)) { 29814 return steeringURI; 29815 } // Return nothing if all valid manifest URIs are excluded. 29816 29817 29818 return null; 29819 } 29820 /** 29821 * Start the timeout for re-requesting the steering manifest at the TTL interval. 29822 * 29823 * @param {number} ttl time in seconds of the timeout. Defaults to the 29824 * ttl interval in the steering manifest 29825 */ 29826 29827 29828 startTTLTimeout_(ttl = this.steeringManifest.ttl) { 29829 // 300 (5 minutes) is the default value. 29830 const ttlMS = ttl * 1000; 29831 this.ttlTimeout_ = window.setTimeout(() => { 29832 this.requestSteeringManifest(); 29833 }, ttlMS); 29834 } 29835 /** 29836 * Clear the TTL timeout if necessary. 29837 */ 29838 29839 29840 clearTTLTimeout_() { 29841 window.clearTimeout(this.ttlTimeout_); 29842 this.ttlTimeout_ = null; 29843 } 29844 /** 29845 * aborts any current steering xhr and sets the current request object to null 29846 */ 29847 29848 29849 abort() { 29850 if (this.request_) { 29851 this.request_.abort(); 29852 } 29853 29854 this.request_ = null; 29855 } 29856 /** 29857 * aborts steering requests clears the ttl timeout and resets all properties. 29858 */ 29859 29860 29861 dispose() { 29862 this.off('content-steering'); 29863 this.off('error'); 29864 this.abort(); 29865 this.clearTTLTimeout_(); 29866 this.currentPathway = null; 29867 this.defaultPathway = null; 29868 this.queryBeforeStart = null; 29869 this.proxyServerUrl_ = null; 29870 this.manifestType_ = null; 29871 this.ttlTimeout_ = null; 29872 this.request_ = null; 29873 this.excludedSteeringManifestURLs = new Set(); 29874 this.availablePathways_ = new Set(); 29875 this.steeringManifest = new SteeringManifest(); 29876 } 29877 /** 29878 * adds a pathway to the available pathways set 29879 * 29880 * @param {string} pathway the pathway string to add 29881 */ 29882 29883 29884 addAvailablePathway(pathway) { 29885 if (pathway) { 29886 this.availablePathways_.add(pathway); 29887 } 29888 } 29889 /** 29890 * Clears all pathways from the available pathways set 29891 */ 29892 29893 29894 clearAvailablePathways() { 29895 this.availablePathways_.clear(); 29896 } 29897 /** 29898 * Removes a pathway from the available pathways set. 29899 */ 29900 29901 29902 excludePathway(pathway) { 29903 return this.availablePathways_.delete(pathway); 29904 } 29905 /** 29906 * Checks the refreshed DASH manifest content steering tag for changes. 29907 * 29908 * @param {string} baseURL new steering tag on DASH manifest refresh 29909 * @param {Object} newTag the new tag to check for changes 29910 * @return a true or false whether the new tag has different values 29911 */ 29912 29913 29914 didDASHTagChange(baseURL, newTag) { 29915 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_); 29916 } 29917 29918 getAvailablePathways() { 29919 return this.availablePathways_; 29920 } 29921 29922 } 29923 29924
vendor: 6,889 bytes, lines 29924-30106
29924/** 29925 * @file playlist-controller.js 29926 */ 29927 const ABORT_EARLY_EXCLUSION_SECONDS = 10; 29928 let Vhs$1; // SegmentLoader stats that need to have each loader's 29929 // values summed to calculate the final value 29930 29931 const loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred', 'mediaAppends']; 29932 29933 const sumLoaderStat = function (stat) { 29934 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 29935 }; 29936 29937 const shouldSwitchToMedia = function ({ 29938 currentPlaylist, 29939 buffered, 29940 currentTime, 29941 nextPlaylist, 29942 bufferLowWaterLine, 29943 bufferHighWaterLine, 29944 duration, 29945 bufferBasedABR, 29946 log 29947 }) { 29948 // we have no other playlist to switch to 29949 if (!nextPlaylist) { 29950 videojs__default["default"].log.warn('We received no playlist to switch to. Please check your stream.'); 29951 return false; 29952 } 29953 29954 const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`; 29955 29956 if (!currentPlaylist) { 29957 log(`${sharedLogLine} as current playlist is not set`); 29958 return true; 29959 } // no need to switch if playlist is the same 29960 29961 29962 if (nextPlaylist.id === currentPlaylist.id) { 29963 return false; 29964 } // determine if current time is in a buffered range. 29965 29966 29967 const isBuffered = Boolean(findRange(buffered, currentTime).length); // If the playlist is live, then we want to not take low water line into account. 29968 // This is because in LIVE, the player plays 3 segments from the end of the 29969 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 29970 // in those segments, a viewer will never experience a rendition upswitch. 29971 29972 if (!currentPlaylist.endList) { 29973 // For LLHLS live streams, don't switch renditions before playback has started, as it almost 29974 // doubles the time to first playback. 29975 if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') { 29976 log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`); 29977 return false; 29978 } 29979 29980 log(`${sharedLogLine} as current playlist is live`); 29981 return true; 29982 } 29983 29984 const forwardBuffer = timeAheadOf(buffered, currentTime); 29985 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 29986 // duration is below the max potential low water line 29987 29988 if (duration < maxBufferLowWaterLine) { 29989 log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`); 29990 return true; 29991 } 29992 29993 const nextBandwidth = nextPlaylist.attributes.BANDWIDTH; 29994 const currBandwidth = currentPlaylist.attributes.BANDWIDTH; // when switching down, if our buffer is lower than the high water line, 29995 // we can switch down 29996 29997 if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) { 29998 let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`; 29999 30000 if (bufferBasedABR) { 30001 logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`; 30002 } 30003 30004 log(logLine); 30005 return true; 30006 } // and if our buffer is higher than the low water line, 30007 // we can switch up 30008 30009 30010 if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) { 30011 let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`; 30012 30013 if (bufferBasedABR) { 30014 logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`; 30015 } 30016 30017 log(logLine); 30018 return true; 30019 } 30020 30021 log(`not ${sharedLogLine} as no switching criteria met`); 30022 return false; 30023 }; 30024 /** 30025 * the main playlist controller controller all interactons 30026 * between playlists and segmentloaders. At this time this mainly 30027 * involves a main playlist and a series of audio playlists 30028 * if they are available 30029 * 30030 * @class PlaylistController 30031 * @extends videojs.EventTarget 30032 */ 30033 30034 30035 class PlaylistController extends videojs__default["default"].EventTarget { 30036 constructor(options) { 30037 super(); 30038 const { 30039 src, 30040 withCredentials, 30041 tech, 30042 bandwidth, 30043 externVhs, 30044 useCueTags, 30045 playlistExclusionDuration, 30046 enableLowInitialPlaylist, 30047 sourceType, 30048 cacheEncryptionKeys, 30049 bufferBasedABR, 30050 leastPixelDiffSelector, 30051 captionServices 30052 } = options; 30053 30054 if (!src) { 30055 throw new Error('A non-empty playlist URL or JSON manifest string is required'); 30056 } 30057 30058 let { 30059 maxPlaylistRetries 30060 } = options; 30061 30062 if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') { 30063 maxPlaylistRetries = Infinity; 30064 } 30065 30066 Vhs$1 = externVhs; 30067 this.bufferBasedABR = Boolean(bufferBasedABR); 30068 this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector); 30069 this.withCredentials = withCredentials; 30070 this.tech_ = tech; 30071 this.vhs_ = tech.vhs; 30072 this.sourceType_ = sourceType; 30073 this.useCueTags_ = useCueTags; 30074 this.playlistExclusionDuration = playlistExclusionDuration; 30075 this.maxPlaylistRetries = maxPlaylistRetries; 30076 this.enableLowInitialPlaylist = enableLowInitialPlaylist; 30077 30078 if (this.useCueTags_) { 30079 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); 30080 this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 30081 } 30082 30083 this.requestOptions_ = { 30084 withCredentials, 30085 maxPlaylistRetries, 30086 timeout: null 30087 }; 30088 this.on('error', this.pauseLoading); 30089 this.mediaTypes_ = createMediaTypes(); 30090 this.mediaSource = new window.MediaSource(); 30091 this.handleDurationChange_ = this.handleDurationChange_.bind(this); 30092 this.handleSourceOpen_ = this.handleSourceOpen_.bind(this); 30093 this.handleSourceEnded_ = this.handleSourceEnded_.bind(this); 30094 this.mediaSource.addEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 30095 30096 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_); 30097 this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_); // we don't have to handle sourceclose since dispose will handle termination of 30098 // everything, and the MediaSource should not be detached without a proper disposal 30099 30100 this.seekable_ = createTimeRanges(); 30101 this.hasPlayed_ = false; 30102 this.syncController_ = new SyncController(options); 30103 this.segmentMetadataTrack_ = tech.addRemoteTextTrack({ 30104 kind: 'metadata', 30105 label: 'segment-metadata' 30106 }, false).track
vendor: 15,826 bytes, lines 30106-30539
30106; 30107 this.decrypter_ = new Decrypter(); 30108 this.sourceUpdater_ = new SourceUpdater(this.mediaSource); 30109 this.inbandTextTracks_ = {}; 30110 this.timelineChangeController_ = new TimelineChangeController(); 30111 this.keyStatusMap_ = new Map(); 30112 const segmentLoaderSettings = { 30113 vhs: this.vhs_, 30114 parse708captions: options.parse708captions, 30115 useDtsForTimestampOffset: options.useDtsForTimestampOffset, 30116 captionServices, 30117 mediaSource: this.mediaSource, 30118 currentTime: this.tech_.currentTime.bind(this.tech_), 30119 seekable: () => this.seekable(), 30120 seeking: () => this.tech_.seeking(), 30121 duration: () => this.duration(), 30122 hasPlayed: () => this.hasPlayed_, 30123 goalBufferLength: () => this.goalBufferLength(), 30124 bandwidth, 30125 syncController: this.syncController_, 30126 decrypter: this.decrypter_, 30127 sourceType: this.sourceType_, 30128 inbandTextTracks: this.inbandTextTracks_, 30129 cacheEncryptionKeys, 30130 sourceUpdater: this.sourceUpdater_, 30131 timelineChangeController: this.timelineChangeController_, 30132 exactManifestTimings: options.exactManifestTimings, 30133 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) 30134 }; // The source type check not only determines whether a special DASH playlist loader 30135 // should be used, but also covers the case where the provided src is a vhs-json 30136 // manifest object (instead of a URL). In the case of vhs-json, the default 30137 // PlaylistLoader should be used. 30138 30139 this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ? new DashPlaylistLoader(src, this.vhs_, merge$1(this.requestOptions_, { 30140 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) 30141 })) : new PlaylistLoader(src, this.vhs_, merge$1(this.requestOptions_, { 30142 addDateRangesToTextTrack: this.addDateRangesToTextTrack_.bind(this) 30143 })); 30144 this.setupMainPlaylistLoaderListeners_(); // setup segment loaders 30145 // combined audio/video or just video when alternate audio track is selected 30146 30147 this.mainSegmentLoader_ = new SegmentLoader(merge$1(segmentLoaderSettings, { 30148 segmentMetadataTrack: this.segmentMetadataTrack_, 30149 loaderType: 'main' 30150 }), options); // alternate audio track 30151 30152 this.audioSegmentLoader_ = new SegmentLoader(merge$1(segmentLoaderSettings, { 30153 loaderType: 'audio' 30154 }), options); 30155 this.subtitleSegmentLoader_ = new VTTSegmentLoader(merge$1(segmentLoaderSettings, { 30156 loaderType: 'vtt', 30157 featuresNativeTextTracks: this.tech_.featuresNativeTextTracks, 30158 loadVttJs: () => new Promise((resolve, reject) => { 30159 function onLoad() { 30160 tech.off('vttjserror', onError); 30161 resolve(); 30162 } 30163 30164 function onError() { 30165 tech.off('vttjsloaded', onLoad); 30166 reject(); 30167 } 30168 30169 tech.one('vttjsloaded', onLoad); 30170 tech.one('vttjserror', onError); // safe to call multiple times, script will be loaded only once: 30171 30172 tech.addWebVttScript_(); 30173 }) 30174 }), options); 30175 30176 const getBandwidth = () => { 30177 return this.mainSegmentLoader_.bandwidth; 30178 }; 30179 30180 this.contentSteeringController_ = new ContentSteeringController(this.vhs_.xhr, getBandwidth); 30181 this.setupSegmentLoaderListeners_(); 30182 30183 if (this.bufferBasedABR) { 30184 this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_()); 30185 this.tech_.on('pause', () => this.stopABRTimer_()); 30186 this.tech_.on('play', () => this.startABRTimer_()); 30187 } // Create SegmentLoader stat-getters 30188 // mediaRequests_ 30189 // mediaRequestsAborted_ 30190 // mediaRequestsTimedout_ 30191 // mediaRequestsErrored_ 30192 // mediaTransferDuration_ 30193 // mediaBytesTransferred_ 30194 // mediaAppends_ 30195 30196 30197 loaderStats.forEach(stat => { 30198 this[stat + '_'] = sumLoaderStat.bind(this, stat); 30199 }); 30200 this.logger_ = logger('pc'); 30201 this.triggeredFmp4Usage = false; 30202 30203 if (this.tech_.preload() === 'none') { 30204 this.loadOnPlay_ = () => { 30205 this.loadOnPlay_ = null; 30206 this.mainPlaylistLoader_.load(); 30207 }; 30208 30209 this.tech_.one('play', this.loadOnPlay_); 30210 } else { 30211 this.mainPlaylistLoader_.load(); 30212 } 30213 30214 this.timeToLoadedData__ = -1; 30215 this.mainAppendsToLoadedData__ = -1; 30216 this.audioAppendsToLoadedData__ = -1; 30217 const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart'; // start the first frame timer on loadstart or play (for preload none) 30218 30219 this.tech_.one(event, () => { 30220 const timeToLoadedDataStart = Date.now(); 30221 this.tech_.one('loadeddata', () => { 30222 this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart; 30223 this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends; 30224 this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends; 30225 }); 30226 }); 30227 } 30228 30229 mainAppendsToLoadedData_() { 30230 return this.mainAppendsToLoadedData__; 30231 } 30232 30233 audioAppendsToLoadedData_() { 30234 return this.audioAppendsToLoadedData__; 30235 } 30236 30237 appendsToLoadedData_() { 30238 const main = this.mainAppendsToLoadedData_(); 30239 const audio = this.audioAppendsToLoadedData_(); 30240 30241 if (main === -1 || audio === -1) { 30242 return -1; 30243 } 30244 30245 return main + audio; 30246 } 30247 30248 timeToLoadedData_() { 30249 return this.timeToLoadedData__; 30250 } 30251 /** 30252 * Run selectPlaylist and switch to the new playlist if we should 30253 * 30254 * @param {string} [reason=abr] a reason for why the ABR check is made 30255 * @private 30256 */ 30257 30258 30259 checkABR_(reason = 'abr') { 30260 const nextPlaylist = this.selectPlaylist(); 30261 30262 if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) { 30263 this.switchMedia_(nextPlaylist, reason); 30264 } 30265 } 30266 30267 switchMedia_(playlist, cause, delay) { 30268 const oldMedia = this.media(); 30269 const oldId = oldMedia && (oldMedia.id || oldMedia.uri); 30270 const newId = playlist && (playlist.id || playlist.uri); 30271 30272 if (oldId && oldId !== newId) { 30273 this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`); 30274 this.tech_.trigger({ 30275 type: 'usage', 30276 name: `vhs-rendition-change-${cause}` 30277 }); 30278 } 30279 30280 this.mainPlaylistLoader_.media(playlist, delay); 30281 } 30282 /** 30283 * A function that ensures we switch our playlists inside of `mediaTypes` 30284 * to match the current `serviceLocation` provided by the contentSteering controller. 30285 * We want to check media types of `AUDIO`, `SUBTITLES`, and `CLOSED-CAPTIONS`. 30286 * 30287 * This should only be called on a DASH playback scenario while using content steering. 30288 * This is necessary due to differences in how media in HLS manifests are generally tied to 30289 * a video playlist, where in DASH that is not always the case. 30290 */ 30291 30292 30293 switchMediaForDASHContentSteering_() { 30294 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 30295 const mediaType = this.mediaTypes_[type]; 30296 const activeGroup = mediaType ? mediaType.activeGroup() : null; 30297 const pathway = this.contentSteeringController_.getPathway(); 30298 30299 if (activeGroup && pathway) { 30300 // activeGroup can be an array or a single group 30301 const mediaPlaylists = activeGroup.length ? activeGroup[0].playlists : activeGroup.playlists; 30302 const dashMediaPlaylists = mediaPlaylists.filter(p => p.attributes.serviceLocation === pathway); // Switch the current active playlist to the correct CDN 30303 30304 if (dashMediaPlaylists.length) { 30305 this.mediaTypes_[type].activePlaylistLoader.media(dashMediaPlaylists[0]); 30306 } 30307 } 30308 }); 30309 } 30310 /** 30311 * Start a timer that periodically calls checkABR_ 30312 * 30313 * @private 30314 */ 30315 30316 30317 startABRTimer_() { 30318 this.stopABRTimer_(); 30319 this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250); 30320 } 30321 /** 30322 * Stop the timer that periodically calls checkABR_ 30323 * 30324 * @private 30325 */ 30326 30327 30328 stopABRTimer_() { 30329 // if we're scrubbing, we don't need to pause. 30330 // This getter will be added to Video.js in version 7.11. 30331 if (this.tech_.scrubbing && this.tech_.scrubbing()) { 30332 return; 30333 } 30334 30335 window.clearInterval(this.abrTimer_); 30336 this.abrTimer_ = null; 30337 } 30338 /** 30339 * Get a list of playlists for the currently selected audio playlist 30340 * 30341 * @return {Array} the array of audio playlists 30342 */ 30343 30344 30345 getAudioTrackPlaylists_() { 30346 const main = this.main(); 30347 const defaultPlaylists = main && main.playlists || []; // if we don't have any audio groups then we can only 30348 // assume that the audio tracks are contained in main 30349 // playlist array, use that or an empty array. 30350 30351 if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) { 30352 return defaultPlaylists; 30353 } 30354 30355 const AUDIO = main.mediaGroups.AUDIO; 30356 const groupKeys = Object.keys(AUDIO); 30357 let track; // get the current active track 30358 30359 if (Object.keys(this.mediaTypes_.AUDIO.groups).length) { 30360 track = this.mediaTypes_.AUDIO.activeTrack(); // or get the default track from main if mediaTypes_ isn't setup yet 30361 } else { 30362 // default group is `main` or just the first group. 30363 const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]]; 30364 30365 for (const label in defaultGroup) { 30366 if (defaultGroup[label].default) { 30367 track = { 30368 label 30369 }; 30370 break; 30371 } 30372 } 30373 } // no active track no playlists. 30374 30375 30376 if (!track) { 30377 return defaultPlaylists; 30378 } 30379 30380 const playlists = []; // get all of the playlists that are possible for the 30381 // active track. 30382 30383 for (const group in AUDIO) { 30384 if (AUDIO[group][track.label]) { 30385 const properties = AUDIO[group][track.label]; 30386 30387 if (properties.playlists && properties.playlists.length) { 30388 playlists.push.apply(playlists, properties.playlists); 30389 } else if (properties.uri) { 30390 playlists.push(properties); 30391 } else if (main.playlists.length) { 30392 // if an audio group does not have a uri 30393 // see if we have main playlists that use it as a group. 30394 // if we do then add those to the playlists list. 30395 for (let i = 0; i < main.playlists.length; i++) { 30396 const playlist = main.playlists[i]; 30397 30398 if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) { 30399 playlists.push(playlist); 30400 } 30401 } 30402 } 30403 } 30404 } 30405 30406 if (!playlists.length) { 30407 return defaultPlaylists; 30408 } 30409 30410 return playlists; 30411 } 30412 /** 30413 * Register event handlers on the main playlist loader. A helper 30414 * function for construction time. 30415 * 30416 * @private 30417 */ 30418 30419 30420 setupMainPlaylistLoaderListeners_() { 30421 this.mainPlaylistLoader_.on('loadedmetadata', () => { 30422 const media = this.mainPlaylistLoader_.media(); 30423 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 30424 // timeout the request. 30425 30426 if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) { 30427 this.requestOptions_.timeout = 0; 30428 } else { 30429 this.requestOptions_.timeout = requestTimeout; 30430 } // if this isn't a live video and preload permits, start 30431 // downloading segments 30432 30433 30434 if (media.endList && this.tech_.preload() !== 'none') { 30435 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 30436 this.mainSegmentLoader_.load(); 30437 } 30438 30439 setupMediaGroups({ 30440 sourceType: this.sourceType_, 30441 segmentLoaders: { 30442 AUDIO: this.audioSegmentLoader_, 30443 SUBTITLES: this.subtitleSegmentLoader_, 30444 main: this.mainSegmentLoader_ 30445 }, 30446 tech: this.tech_, 30447 requestOptions: this.requestOptions_, 30448 mainPlaylistLoader: this.mainPlaylistLoader_, 30449 vhs: this.vhs_, 30450 main: this.main(), 30451 mediaTypes: this.mediaTypes_, 30452 excludePlaylist: this.excludePlaylist.bind(this) 30453 }); 30454 this.triggerPresenceUsage_(this.main(), media); 30455 this.setupFirstPlay(); 30456 30457 if (!this.mediaTypes_.AUDIO.activePlaylistLoader || this.mediaTypes_.AUDIO.activePlaylistLoader.media()) { 30458 this.trigger('selectedinitialmedia'); 30459 } else { 30460 // We must wait for the active audio playlist loader to 30461 // finish setting up before triggering this event so the 30462 // representations API and EME setup is correct 30463 this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => { 30464 this.trigger('selectedinitialmedia'); 30465 }); 30466 } 30467 }); 30468 this.mainPlaylistLoader_.on('loadedplaylist', () => { 30469 if (this.loadOnPlay_) { 30470 this.tech_.off('play', this.loadOnPlay_); 30471 } 30472 30473 let updatedPlaylist = this.mainPlaylistLoader_.media(); 30474 30475 if (!updatedPlaylist) { 30476 // Add content steering listeners on first load and init. 30477 this.attachContentSteeringListeners_(); 30478 this.initContentSteeringController_(); // exclude any variants that are not supported by the browser before selecting 30479 // an initial media as the playlist selectors do not consider browser support 30480 30481 this.excludeUnsupportedVariants_(); 30482 let selectedMedia; 30483 30484 if (this.enableLowInitialPlaylist) { 30485 selectedMedia = this.selectInitialPlaylist(); 30486 } 30487 30488 if (!selectedMedia) { 30489 selectedMedia = this.selectPlaylist(); 30490 } 30491 30492 if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) { 30493 return; 30494 } 30495 30496 this.initialMedia_ = selectedMedia; 30497 this.switchMedia_(this.initialMedia_, 'initial'); // Under the standard case where a source URL is provided, loadedplaylist will 30498 // fire again since the playlist will be requested. In the case of vhs-json 30499 // (where the manifest object is provided as the source), when the media 30500 // playlist's `segments` list is already available, a media playlist won't be 30501 // requested, and loadedplaylist won't fire again, so the playlist handler must be 30502 // called on its own here. 30503 30504 const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments; 30505 30506 if (!haveJsonSource) { 30507 return; 30508 } 30509 30510 updatedPlaylist = this.initialMedia_; 30511 } 30512 30513 this.handleUpdatedMediaPlaylist(updatedPlaylist); 30514 }); 30515 this.mainPlaylistLoader_.on('error', () => { 30516 const error = this.mainPlaylistLoader_.error; 30517 this.excludePlaylist({ 30518 playlistToExclude: error.playlist, 30519 error 30520 }); 30521 }); 30522 this.mainPlaylistLoader_.on('mediachanging', () => { 30523 this.mainSegmentLoader_.abort(); 30524 this.mainSegmentLoader_.pause(); 30525 }); 30526 this.mainPlaylistLoader_.on('mediachange', () => { 30527 const media = this.mainPlaylistLoader_.media(); 30528 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 30529 // timeout the request. 30530 30531 if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) { 30532 this.requestOptions_.timeout = 0; 30533 } else { 30534 this.requestOptions_.timeout = requestTimeout; 30535 } 30536 30537 if (this.sourceType_ === 'dash') { 30538 // we don't want to re-request the same hls playlist right after it was changed 30539
30539this.mainPlaylistLoader_.load(); 30540 } // TODO: Create a new event on the PlaylistLoader that signals 30541 // that the segments have changed in some way and use that to 30542 // update the SegmentLoader instead of doing it twice here and 30543 // on `loadedplaylist` 30544 30545 30546 this.mainSegmentLoader_.pause(); 30547 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 30548 30549 if (this.waitingForFastQualityPlaylistReceived_) { 30550 this.runFastQualitySwitch_(); 30551 } else { 30552 this.mainSegmentLoader_.load(); 30553 } 30554 30555 this.tech_.trigger({ 30556 type: 'mediachange', 30557 bubbles: true 30558 }); 30559 }); 30560 this.mainPlaylistLoader_.on('playlistunchanged', () => { 30561 const updatedPlaylist = this.mainPlaylistLoader_.media(); // ignore unchanged playlists that have already been 30562 // excluded for not-changing. We likely just have a really slowly updating 30563 // playlist. 30564 30565 if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') { 30566 return; 30567 } 30568 30569 const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist); 30570 30571 if (playlistOutdated) { 30572 // Playlist has stopped updating and we're stuck at its end. Try to 30573 // exclude it and switch to another playlist in the hope that that 30574 // one is updating (and give the player a chance to re-adjust to the 30575 // safe live point). 30576 this.excludePlaylist({ 30577 error: { 30578 message: 'Playlist no longer updating.', 30579 reason: 'playlist-unchanged' 30580 } 30581 }); // useful for monitoring QoS 30582 30583 this.tech_.trigger('playliststuck'); 30584 } 30585 }); 30586 this.mainPlaylistLoader_.on('renditiondisabled', () => { 30587 this.tech_.trigger({ 30588 type: 'usage', 30589 name: 'vhs-rendition-disabled' 30590 }); 30591 }); 30592 this.mainPlaylistLoader_.on('renditionenabled', () => { 30593 this.tech_.trigger({ 30594 type: 'usage', 30595 name: 'vhs-rendition-enabled'
vendor: 5,824 bytes, lines 30595-30776
30595 30596 }); 30597 }); 30598 } 30599 /** 30600 * Given an updated media playlist (whether it was loaded for the first time, or 30601 * refreshed for live playlists), update any relevant properties and state to reflect 30602 * changes in the media that should be accounted for (e.g., cues and duration). 30603 * 30604 * @param {Object} updatedPlaylist the updated media playlist object 30605 * 30606 * @private 30607 */ 30608 30609 30610 handleUpdatedMediaPlaylist(updatedPlaylist) { 30611 if (this.useCueTags_) { 30612 this.updateAdCues_(updatedPlaylist); 30613 } // TODO: Create a new event on the PlaylistLoader that signals 30614 // that the segments have changed in some way and use that to 30615 // update the SegmentLoader instead of doing it twice here and 30616 // on `mediachange` 30617 30618 30619 this.mainSegmentLoader_.pause(); 30620 this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_); 30621 30622 if (this.waitingForFastQualityPlaylistReceived_) { 30623 this.runFastQualitySwitch_(); 30624 } 30625 30626 this.updateDuration(!updatedPlaylist.endList); // If the player isn't paused, ensure that the segment loader is running, 30627 // as it is possible that it was temporarily stopped while waiting for 30628 // a playlist (e.g., in case the playlist errored and we re-requested it). 30629 30630 if (!this.tech_.paused()) { 30631 this.mainSegmentLoader_.load(); 30632 30633 if (this.audioSegmentLoader_) { 30634 this.audioSegmentLoader_.load(); 30635 } 30636 } 30637 } 30638 /** 30639 * A helper function for triggerring presence usage events once per source 30640 * 30641 * @private 30642 */ 30643 30644 30645 triggerPresenceUsage_(main, media) { 30646 const mediaGroups = main.mediaGroups || {}; 30647 let defaultDemuxed = true; 30648 const audioGroupKeys = Object.keys(mediaGroups.AUDIO); 30649 30650 for (const mediaGroup in mediaGroups.AUDIO) { 30651 for (const label in mediaGroups.AUDIO[mediaGroup]) { 30652 const properties = mediaGroups.AUDIO[mediaGroup][label]; 30653 30654 if (!properties.uri) { 30655 defaultDemuxed = false; 30656 } 30657 } 30658 } 30659 30660 if (defaultDemuxed) { 30661 this.tech_.trigger({ 30662 type: 'usage', 30663 name: 'vhs-demuxed' 30664 }); 30665 } 30666 30667 if (Object.keys(mediaGroups.SUBTITLES).length) { 30668 this.tech_.trigger({ 30669 type: 'usage', 30670 name: 'vhs-webvtt' 30671 }); 30672 } 30673 30674 if (Vhs$1.Playlist.isAes(media)) { 30675 this.tech_.trigger({ 30676 type: 'usage', 30677 name: 'vhs-aes' 30678 }); 30679 } 30680 30681 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 30682 this.tech_.trigger({ 30683 type: 'usage', 30684 name: 'vhs-alternate-audio' 30685 }); 30686 } 30687 30688 if (this.useCueTags_) { 30689 this.tech_.trigger({ 30690 type: 'usage', 30691 name: 'vhs-playlist-cue-tags' 30692 }); 30693 } 30694 } 30695 30696 shouldSwitchToMedia_(nextPlaylist) { 30697 const currentPlaylist = this.mainPlaylistLoader_.media() || this.mainPlaylistLoader_.pendingMedia_; 30698 const currentTime = this.tech_.currentTime(); 30699 const bufferLowWaterLine = this.bufferLowWaterLine(); 30700 const bufferHighWaterLine = this.bufferHighWaterLine(); 30701 const buffered = this.tech_.buffered(); 30702 return shouldSwitchToMedia({ 30703 buffered, 30704 currentTime, 30705 currentPlaylist, 30706 nextPlaylist, 30707 bufferLowWaterLine, 30708 bufferHighWaterLine, 30709 duration: this.duration(), 30710 bufferBasedABR: this.bufferBasedABR, 30711 log: this.logger_ 30712 }); 30713 } 30714 /** 30715 * Register event handlers on the segment loaders. A helper function 30716 * for construction time. 30717 * 30718 * @private 30719 */ 30720 30721 30722 setupSegmentLoaderListeners_() { 30723 this.mainSegmentLoader_.on('bandwidthupdate', () => { 30724 // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's 30725 // useful to check to see if a rendition switch should be made. 30726 this.checkABR_('bandwidthupdate'); 30727 this.tech_.trigger('bandwidthupdate'); 30728 }); 30729 this.mainSegmentLoader_.on('timeout', () => { 30730 if (this.bufferBasedABR) { 30731 // If a rendition change is needed, then it would've be done on `bandwidthupdate`. 30732 // Here the only consideration is that for buffer based ABR there's no guarantee 30733 // of an immediate switch (since the bandwidth is averaged with a timeout 30734 // bandwidth value of 1), so force a load on the segment loader to keep it going. 30735 this.mainSegmentLoader_.load(); 30736 } 30737 }); // `progress` events are not reliable enough of a bandwidth measure to trigger buffer 30738 // based ABR. 30739 30740 if (!this.bufferBasedABR) { 30741 this.mainSegmentLoader_.on('progress', () => { 30742 this.trigger('progress'); 30743 }); 30744 } 30745 30746 this.mainSegmentLoader_.on('error', () => { 30747 const error = this.mainSegmentLoader_.error(); 30748 this.excludePlaylist({ 30749 playlistToExclude: error.playlist, 30750 error 30751 }); 30752 }); 30753 this.mainSegmentLoader_.on('appenderror', () => { 30754 this.error = this.mainSegmentLoader_.error_; 30755 this.trigger('error'); 30756 }); 30757 this.mainSegmentLoader_.on('syncinfoupdate', () => { 30758 this.onSyncInfoUpdate_(); 30759 }); 30760 this.mainSegmentLoader_.on('timestampoffset', () => { 30761 this.tech_.trigger({ 30762 type: 'usage', 30763 name: 'vhs-timestamp-offset' 30764 }); 30765 }); 30766 this.audioSegmentLoader_.on('syncinfoupdate', () => { 30767 this.onSyncInfoUpdate_(); 30768 }); 30769 this.audioSegmentLoader_.on('appenderror', () => { 30770 this.error = this.audioSegmentLoader_.error_; 30771 this.trigger('error'); 30772 }); 30773 this.mainSegmentLoader_.on('ended', () => { 30774 this.logger_('main segment loader ended'); 30775 this.onEndOfStream(); 30776 });
vendor: 5,619 bytes, lines 30776-30947
30776 30777 this.mainSegmentLoader_.on('earlyabort', event => { 30778 // never try to early abort with the new ABR algorithm 30779 if (this.bufferBasedABR) { 30780 return; 30781 } 30782 30783 this.delegateLoaders_('all', ['abort']); 30784 this.excludePlaylist({ 30785 error: { 30786 message: 'Aborted early because there isn\'t enough bandwidth to complete ' + 'the request without rebuffering.' 30787 }, 30788 playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS 30789 }); 30790 }); 30791 30792 const updateCodecs = () => { 30793 if (!this.sourceUpdater_.hasCreatedSourceBuffers()) { 30794 return this.tryToCreateSourceBuffers_(); 30795 } 30796 30797 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 30798 30799 if (!codecs) { 30800 return; 30801 } 30802 30803 this.sourceUpdater_.addOrChangeSourceBuffers(codecs); 30804 }; 30805 30806 this.mainSegmentLoader_.on('trackinfo', updateCodecs); 30807 this.audioSegmentLoader_.on('trackinfo', updateCodecs); 30808 this.mainSegmentLoader_.on('fmp4', () => { 30809 if (!this.triggeredFmp4Usage) { 30810 this.tech_.trigger({ 30811 type: 'usage', 30812 name: 'vhs-fmp4' 30813 }); 30814 this.triggeredFmp4Usage = true; 30815 } 30816 }); 30817 this.audioSegmentLoader_.on('fmp4', () => { 30818 if (!this.triggeredFmp4Usage) { 30819 this.tech_.trigger({ 30820 type: 'usage', 30821 name: 'vhs-fmp4' 30822 }); 30823 this.triggeredFmp4Usage = true; 30824 } 30825 }); 30826 this.audioSegmentLoader_.on('ended', () => { 30827 this.logger_('audioSegmentLoader ended'); 30828 this.onEndOfStream(); 30829 }); 30830 } 30831 30832 mediaSecondsLoaded_() { 30833 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded); 30834 } 30835 /** 30836 * Call load on our SegmentLoaders 30837 */ 30838 30839 30840 load() { 30841 this.mainSegmentLoader_.load(); 30842 30843 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 30844 this.audioSegmentLoader_.load(); 30845 } 30846 30847 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 30848 this.subtitleSegmentLoader_.load(); 30849 } 30850 } 30851 /** 30852 * Re-tune playback quality level for the current player 30853 * conditions. This method will perform destructive actions like removing 30854 * already buffered content in order to readjust the currently active 30855 * playlist quickly. This is good for manual quality changes 30856 * 30857 * @private 30858 */ 30859 30860 30861 fastQualityChange_(media = this.selectPlaylist()) { 30862 if (media && media === this.mainPlaylistLoader_.media()) { 30863 this.logger_('skipping fastQualityChange because new media is same as old'); 30864 return; 30865 } 30866 30867 this.switchMedia_(media, 'fast-quality'); // we would like to avoid race condition when we call fastQuality, 30868 // reset everything and start loading segments from prev segments instead of new because new playlist is not received yet 30869 30870 this.waitingForFastQualityPlaylistReceived_ = true; 30871 } 30872 30873 runFastQualitySwitch_() { 30874 this.waitingForFastQualityPlaylistReceived_ = false; // Delete all buffered data to allow an immediate quality switch, then seek to give 30875 // the browser a kick to remove any cached frames from the previous rendtion (.04 seconds 30876 // ahead was roughly the minimum that will accomplish this across a variety of content 30877 // in IE and Edge, but seeking in place is sufficient on all other browsers) 30878 // Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/ 30879 // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904 30880 30881 this.mainSegmentLoader_.pause(); 30882 this.mainSegmentLoader_.resetEverything(() => { 30883 this.tech_.setCurrentTime(this.tech_.currentTime()); 30884 }); // don't need to reset audio as it is reset when media changes 30885 } 30886 /** 30887 * Begin playback. 30888 */ 30889 30890 30891 play() { 30892 if (this.setupFirstPlay()) { 30893 return; 30894 } 30895 30896 if (this.tech_.ended()) { 30897 this.tech_.setCurrentTime(0); 30898 } 30899 30900 if (this.hasPlayed_) { 30901 this.load(); 30902 } 30903 30904 const seekable = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window, 30905 // seek forward to the live point 30906 30907 if (this.tech_.duration() === Infinity) { 30908 if (this.tech_.currentTime() < seekable.start(0)) { 30909 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1)); 30910 } 30911 } 30912 } 30913 /** 30914 * Seek to the latest media position if this is a live video and the 30915 * player and video are loaded and initialized. 30916 */ 30917 30918 30919 setupFirstPlay() { 30920 const media = this.mainPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play 30921 // If 1) there is no active media 30922 // 2) the player is paused 30923 // 3) the first play has already been setup 30924 // then exit early 30925 30926 if (!media || this.tech_.paused() || this.hasPlayed_) { 30927 return false; 30928 } // when the video is a live stream and/or has a start time 30929 30930 30931 if (!media.endList || media.start) { 30932 const seekable = this.seekable(); 30933 30934 if (!seekable.length) { 30935 // without a seekable range, the player cannot seek to begin buffering at the 30936 // live or start point 30937 return false; 30938 } 30939 30940 const seekableEnd = seekable.end(0); 30941 let startPoint = seekableEnd; 30942 30943 if (media.start) { 30944 const offset = media.start.timeOffset; 30945 30946 if (offset < 0) { 30947 startPoint = Math.max(seekableEnd + offset
vendor: 16,024 bytes, lines 30947-31398
30947, seekable.start(0)); 30948 } else { 30949 startPoint = Math.min(seekableEnd, offset); 30950 } 30951 } // trigger firstplay to inform the source handler to ignore the next seek event 30952 30953 30954 this.trigger('firstplay'); // seek to the live point 30955 30956 this.tech_.setCurrentTime(startPoint); 30957 } 30958 30959 this.hasPlayed_ = true; // we can begin loading now that everything is ready 30960 30961 this.load(); 30962 return true; 30963 } 30964 /** 30965 * handle the sourceopen event on the MediaSource 30966 * 30967 * @private 30968 */ 30969 30970 30971 handleSourceOpen_() { 30972 // Only attempt to create the source buffer if none already exist. 30973 // handleSourceOpen is also called when we are "re-opening" a source buffer 30974 // after `endOfStream` has been called (in response to a seek for instance) 30975 this.tryToCreateSourceBuffers_(); // if autoplay is enabled, begin playback. This is duplicative of 30976 // code in video.js but is required because play() must be invoked 30977 // *after* the media source has opened. 30978 30979 if (this.tech_.autoplay()) { 30980 const playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request 30981 // on browsers which return a promise 30982 30983 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 30984 playPromise.then(null, e => {}); 30985 } 30986 } 30987 30988 this.trigger('sourceopen'); 30989 } 30990 /** 30991 * handle the sourceended event on the MediaSource 30992 * 30993 * @private 30994 */ 30995 30996 30997 handleSourceEnded_() { 30998 if (!this.inbandTextTracks_.metadataTrack_) { 30999 return; 31000 } 31001 31002 const cues = this.inbandTextTracks_.metadataTrack_.cues; 31003 31004 if (!cues || !cues.length) { 31005 return; 31006 } 31007 31008 const duration = this.duration(); 31009 cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ? Number.MAX_VALUE : duration; 31010 } 31011 /** 31012 * handle the durationchange event on the MediaSource 31013 * 31014 * @private 31015 */ 31016 31017 31018 handleDurationChange_() { 31019 this.tech_.trigger('durationchange'); 31020 } 31021 /** 31022 * Calls endOfStream on the media source when all active stream types have called 31023 * endOfStream 31024 * 31025 * @param {string} streamType 31026 * Stream type of the segment loader that called endOfStream 31027 * @private 31028 */ 31029 31030 31031 onEndOfStream() { 31032 let isEndOfStream = this.mainSegmentLoader_.ended_; 31033 31034 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 31035 const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_(); // if the audio playlist loader exists, then alternate audio is active 31036 31037 if (!mainMediaInfo || mainMediaInfo.hasVideo) { 31038 // if we do not know if the main segment loader contains video yet or if we 31039 // definitively know the main segment loader contains video, then we need to wait 31040 // for both main and audio segment loaders to call endOfStream 31041 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 31042 } else { 31043 // otherwise just rely on the audio loader 31044 isEndOfStream = this.audioSegmentLoader_.ended_; 31045 } 31046 } 31047 31048 if (!isEndOfStream) { 31049 return; 31050 } 31051 31052 this.stopABRTimer_(); 31053 this.sourceUpdater_.endOfStream(); 31054 } 31055 /** 31056 * Check if a playlist has stopped being updated 31057 * 31058 * @param {Object} playlist the media playlist object 31059 * @return {boolean} whether the playlist has stopped being updated or not 31060 */ 31061 31062 31063 stuckAtPlaylistEnd_(playlist) { 31064 const seekable = this.seekable(); 31065 31066 if (!seekable.length) { 31067 // playlist doesn't have enough information to determine whether we are stuck 31068 return false; 31069 } 31070 31071 const expired = this.syncController_.getExpiredTime(playlist, this.duration()); 31072 31073 if (expired === null) { 31074 return false; 31075 } // does not use the safe live end to calculate playlist end, since we 31076 // don't want to say we are stuck while there is still content 31077 31078 31079 const absolutePlaylistEnd = Vhs$1.Playlist.playlistEnd(playlist, expired); 31080 const currentTime = this.tech_.currentTime(); 31081 const buffered = this.tech_.buffered(); 31082 31083 if (!buffered.length) { 31084 // return true if the playhead reached the absolute end of the playlist 31085 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 31086 } 31087 31088 const bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute 31089 // end of playlist 31090 31091 return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA; 31092 } 31093 /** 31094 * Exclude a playlist for a set amount of time, making it unavailable for selection by 31095 * the rendition selection algorithm, then force a new playlist (rendition) selection. 31096 * 31097 * @param {Object=} playlistToExclude 31098 * the playlist to exclude, defaults to the currently selected playlist 31099 * @param {Object=} error 31100 * an optional error 31101 * @param {number=} playlistExclusionDuration 31102 * an optional number of seconds to exclude the playlist 31103 */ 31104 31105 31106 excludePlaylist({ 31107 playlistToExclude = this.mainPlaylistLoader_.media(), 31108 error = {}, 31109 playlistExclusionDuration 31110 }) { 31111 // If the `error` was generated by the playlist loader, it will contain 31112 // the playlist we were trying to load (but failed) and that should be 31113 // excluded instead of the currently selected playlist which is likely 31114 // out-of-date in this scenario 31115 playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media(); 31116 playlistExclusionDuration = playlistExclusionDuration || error.playlistExclusionDuration || this.playlistExclusionDuration; // If there is no current playlist, then an error occurred while we were 31117 // trying to load the main OR while we were disposing of the tech 31118 31119 if (!playlistToExclude) { 31120 this.error = error; 31121 31122 if (this.mediaSource.readyState !== 'open') { 31123 this.trigger('error'); 31124 } else { 31125 this.sourceUpdater_.endOfStream('network'); 31126 } 31127 31128 return; 31129 } 31130 31131 playlistToExclude.playlistErrors_++; 31132 const playlists = this.mainPlaylistLoader_.main.playlists; 31133 const enabledPlaylists = playlists.filter(isEnabled); 31134 const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded 31135 // forever 31136 31137 if (playlists.length === 1 && playlistExclusionDuration !== Infinity) { 31138 videojs__default["default"].log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` + 'Trying again since it is the only playlist.'); 31139 this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay 31140 31141 return this.mainPlaylistLoader_.load(isFinalRendition); 31142 } 31143 31144 if (isFinalRendition) { 31145 // If we're content steering, try other pathways. 31146 if (this.main().contentSteering) { 31147 const pathway = this.pathwayAttribute_(playlistToExclude); // Ignore at least 1 steering manifest refresh. 31148 31149 const reIncludeDelay = this.contentSteeringController_.steeringManifest.ttl * 1000; 31150 this.contentSteeringController_.excludePathway(pathway); 31151 this.excludeThenChangePathway_(); 31152 setTimeout(() => { 31153 this.contentSteeringController_.addAvailablePathway(pathway); 31154 }, reIncludeDelay); 31155 return; 31156 } // Since we're on the final non-excluded playlist, and we're about to exclude 31157 // it, instead of erring the player or retrying this playlist, clear out the current 31158 // exclusion list. This allows other playlists to be attempted in case any have been 31159 // fixed. 31160 31161 31162 let reincluded = false; 31163 playlists.forEach(playlist => { 31164 // skip current playlist which is about to be excluded 31165 if (playlist === playlistToExclude) { 31166 return; 31167 } 31168 31169 const excludeUntil = playlist.excludeUntil; // a playlist cannot be reincluded if it wasn't excluded to begin with. 31170 31171 if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) { 31172 reincluded = true; 31173 delete playlist.excludeUntil; 31174 } 31175 }); 31176 31177 if (reincluded) { 31178 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 31179 // playlist. This is needed for users relying on the retryplaylist event to catch a 31180 // case where the player might be stuck and looping through "dead" playlists. 31181 31182 this.tech_.trigger('retryplaylist'); 31183 } 31184 } // Exclude this playlist 31185 31186 31187 let excludeUntil; 31188 31189 if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) { 31190 excludeUntil = Infinity; 31191 } else { 31192 excludeUntil = Date.now() + playlistExclusionDuration * 1000; 31193 } 31194 31195 playlistToExclude.excludeUntil = excludeUntil; 31196 31197 if (error.reason) { 31198 playlistToExclude.lastExcludeReason_ = error.reason; 31199 } 31200 31201 this.tech_.trigger('excludeplaylist'); 31202 this.tech_.trigger({ 31203 type: 'usage', 31204 name: 'vhs-rendition-excluded' 31205 }); // TODO: only load a new playlist if we're excluding the current playlist 31206 // If this function was called with a playlist that's not the current active playlist 31207 // (e.g., media().id !== playlistToExclude.id), 31208 // then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist. 31209 31210 const nextPlaylist = this.selectPlaylist(); 31211 31212 if (!nextPlaylist) { 31213 this.error = 'Playback cannot continue. No available working or supported playlists.'; 31214 this.trigger('error'); 31215 return; 31216 } 31217 31218 const logFn = error.internal ? this.logger_ : videojs__default["default"].log.warn; 31219 const errorMessage = error.message ? ' ' + error.message : ''; 31220 logFn(`${error.internal ? 'Internal problem' : 'Problem'} encountered with playlist ${playlistToExclude.id}.` + `${errorMessage} Switching to playlist ${nextPlaylist.id}.`); // if audio group changed reset audio loaders 31221 31222 if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) { 31223 this.delegateLoaders_('audio', ['abort', 'pause']); 31224 } // if subtitle group changed reset subtitle loaders 31225 31226 31227 if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) { 31228 this.delegateLoaders_('subtitle', ['abort', 'pause']); 31229 } 31230 31231 this.delegateLoaders_('main', ['abort', 'pause']); 31232 const delayDuration = nextPlaylist.targetDuration / 2 * 1000 || 5 * 1000; 31233 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 31234 31235 return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay); 31236 } 31237 /** 31238 * Pause all segment/playlist loaders 31239 */ 31240 31241 31242 pauseLoading() { 31243 this.delegateLoaders_('all', ['abort', 'pause']); 31244 this.stopABRTimer_(); 31245 } 31246 /** 31247 * Call a set of functions in order on playlist loaders, segment loaders, 31248 * or both types of loaders. 31249 * 31250 * @param {string} filter 31251 * Filter loaders that should call fnNames using a string. Can be: 31252 * * all - run on all loaders 31253 * * audio - run on all audio loaders 31254 * * subtitle - run on all subtitle loaders 31255 * * main - run on the main loaders 31256 * 31257 * @param {Array|string} fnNames 31258 * A string or array of function names to call. 31259 */ 31260 31261 31262 delegateLoaders_(filter, fnNames) { 31263 const loaders = []; 31264 const dontFilterPlaylist = filter === 'all'; 31265 31266 if (dontFilterPlaylist || filter === 'main') { 31267 loaders.push(this.mainPlaylistLoader_); 31268 } 31269 31270 const mediaTypes = []; 31271 31272 if (dontFilterPlaylist || filter === 'audio') { 31273 mediaTypes.push('AUDIO'); 31274 } 31275 31276 if (dontFilterPlaylist || filter === 'subtitle') { 31277 mediaTypes.push('CLOSED-CAPTIONS'); 31278 mediaTypes.push('SUBTITLES'); 31279 } 31280 31281 mediaTypes.forEach(mediaType => { 31282 const loader = this.mediaTypes_[mediaType] && this.mediaTypes_[mediaType].activePlaylistLoader; 31283 31284 if (loader) { 31285 loaders.push(loader); 31286 } 31287 }); 31288 ['main', 'audio', 'subtitle'].forEach(name => { 31289 const loader = this[`${name}SegmentLoader_`]; 31290 31291 if (loader && (filter === name || filter === 'all')) { 31292 loaders.push(loader); 31293 } 31294 }); 31295 loaders.forEach(loader => fnNames.forEach(fnName => { 31296 if (typeof loader[fnName] === 'function') { 31297 loader[fnName](); 31298 } 31299 })); 31300 } 31301 /** 31302 * set the current time on all segment loaders 31303 * 31304 * @param {TimeRange} currentTime the current time to set 31305 * @return {TimeRange} the current time 31306 */ 31307 31308 31309 setCurrentTime(currentTime) { 31310 const buffered = findRange(this.tech_.buffered(), currentTime); 31311 31312 if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) { 31313 // return immediately if the metadata is not ready yet 31314 return 0; 31315 } // it's clearly an edge-case but don't thrown an error if asked to 31316 // seek within an empty playlist 31317 31318 31319 if (!this.mainPlaylistLoader_.media().segments) { 31320 return 0; 31321 } // if the seek location is already buffered, continue buffering as usual 31322 31323 31324 if (buffered && buffered.length) { 31325 return currentTime; 31326 } // cancel outstanding requests so we begin buffering at the new 31327 // location 31328 31329 31330 this.mainSegmentLoader_.pause(); 31331 this.mainSegmentLoader_.resetEverything(); 31332 31333 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 31334 this.audioSegmentLoader_.pause(); 31335 this.audioSegmentLoader_.resetEverything(); 31336 } 31337 31338 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 31339 this.subtitleSegmentLoader_.pause(); 31340 this.subtitleSegmentLoader_.resetEverything(); 31341 } // start segment loader loading in case they are paused 31342 31343 31344 this.load(); 31345 } 31346 /** 31347 * get the current duration 31348 * 31349 * @return {TimeRange} the duration 31350 */ 31351 31352 31353 duration() { 31354 if (!this.mainPlaylistLoader_) { 31355 return 0; 31356 } 31357 31358 const media = this.mainPlaylistLoader_.media(); 31359 31360 if (!media) { 31361 // no playlists loaded yet, so can't determine a duration 31362 return 0; 31363 } // Don't rely on the media source for duration in the case of a live playlist since 31364 // setting the native MediaSource's duration to infinity ends up with consequences to 31365 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 31366 // 31367 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 31368 // however, few browsers have support for setLiveSeekableRange() 31369 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 31370 // 31371 // Until a time when the duration of the media source can be set to infinity, and a 31372 // seekable range specified across browsers, just return Infinity. 31373 31374 31375 if (!media.endList) { 31376 return Infinity; 31377 } // Since this is a VOD video, it is safe to rely on the media source's duration (if 31378 // available). If it's not available, fall back to a playlist-calculated estimate. 31379 31380 31381 if (this.mediaSource) { 31382 return this.mediaSource.duration; 31383 } 31384 31385 return Vhs$1.Playlist.duration(media); 31386 } 31387 /** 31388 * check the seekable range 31389 * 31390 * @return {TimeRange} the seekable range 31391 */ 31392 31393 31394 seekable() { 31395 return this.seekable_; 31396 } 31397 31398
31398onSyncInfoUpdate_() { 31399 let audioSeekable; // TODO check for creation of both source buffers before updating seekable 31400 // 31401 // A fix was made to this function where a check for 31402 // this.sourceUpdater_.hasCreatedSourceBuffers 31403 // was added to ensure that both source buffers were created before seekable was 31404 // updated. However, it originally had a bug where it was checking for a true and 31405 // returning early instead of checking for false. Setting it to check for false to 31406 // return early though created other issues. A call to play() would check for seekable 31407 // end without verifying that a seekable range was present. In addition, even checking 31408 // for that didn't solve some issues, as handleFirstPlay is sometimes worked around 31409 // due to a media update calling load on the segment loaders, skipping a seek to live, 31410 // thereby starting live streams at the beginning of the stream rather than at the end. 31411 // 31412 // This conditional should be fixed to wait for the creation of two source buffers at 31413 // the same time as the other sections of code are fixed to properly seek to live and 31414 // not throw an error due to checking for a seekable end when no seekable range exists. 31415 // 31416 // For now, fall back to the older behavior, with the understanding that the seekable 31417 // range may not be completely correct, leading to a suboptimal initial live point. 31418 31419 if (!this.mainPlaylistLoader_) { 31420 return; 31421 } 31422 31423 let media = this.mainPlaylistLoader_.media(); 31424 31425 if (!media) { 31426 return; 31427 } 31428 31429 let expired = this.syncController_.getExpiredTime(media, this.duration()); 31430 31431 if (expired === null) { 31432 // not enough information to update seekable 31433 return; 31434 } 31435 31436 const main = this.mainPlaylistLoader_.main; 31437 const mainSeekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(main, media)); 31438 31439 if (mainSeekable.length === 0) { 31440 return; 31441 } 31442 31443 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 31444 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media(); 31445 expired = this.syncController_.getExpiredTime(media, this.duration()); 31446 31447 if (expired === null) { 31448 return; 31449 } 31450 31451 audioSeekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(main, media)); 31452 31453 if (audioSeekable.length === 0) { 31454 return; 31455 } 31456 } 31457 31458 let oldEnd; 31459 let oldStart; 31460 31461 if (this.seekable_ && this.seekable_.length) { 31462 oldEnd = this.seekable_.end(0); 31463 oldStart = this.seekable_.start(0); 31464 } 31465 31466 if (!audioSeekable) { 31467 // seekable has been calculated based on buffering video data so it 31468 // can be returned directly 31469 this.seekable_ = mainSeekable; 31470 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) { 31471 // seekables are pretty far off, rely on main 31472 this.seekable_ = mainSeekable; 31473 } else { 31474 this.seekable_ = createTimeRanges([[audioSeekable.start(0) > mainSeekable.start(0) ? audioSeekable.start(0) : mainSeekable.start(0), audioSeekable.end(0) < mainSeekable.end(0) ? audioSeekable.end(0) : mainSeekable.end(0)]]); 31475 } // seekable is the same as last time 31476 31477 31478 if (this.seekable_ && this.seekable_.length) { 31479 if (this.seekable_.end(0) === oldEnd && this.seekable_.start(0) === oldStart) { 31480 return; 31481 } 31482 } 31483 31484 this.logger_(`seekable updated [${printableRange(this.seekable_)}]`
vendor: 5,532 bytes, lines 31484-31631
31484); 31485 this.tech_.trigger('seekablechanged'); 31486 } 31487 /** 31488 * Update the player duration 31489 */ 31490 31491 31492 updateDuration(isLive) { 31493 if (this.updateDuration_) { 31494 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 31495 this.updateDuration_ = null; 31496 } 31497 31498 if (this.mediaSource.readyState !== 'open') { 31499 this.updateDuration_ = this.updateDuration.bind(this, isLive); 31500 this.mediaSource.addEventListener('sourceopen', this.updateDuration_); 31501 return; 31502 } 31503 31504 if (isLive) { 31505 const seekable = this.seekable(); 31506 31507 if (!seekable.length) { 31508 return; 31509 } // Even in the case of a live playlist, the native MediaSource's duration should not 31510 // be set to Infinity (even though this would be expected for a live playlist), since 31511 // setting the native MediaSource's duration to infinity ends up with consequences to 31512 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 31513 // 31514 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 31515 // however, few browsers have support for setLiveSeekableRange() 31516 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 31517 // 31518 // Until a time when the duration of the media source can be set to infinity, and a 31519 // seekable range specified across browsers, the duration should be greater than or 31520 // equal to the last possible seekable value. 31521 // MediaSource duration starts as NaN 31522 // It is possible (and probable) that this case will never be reached for many 31523 // sources, since the MediaSource reports duration as the highest value without 31524 // accounting for timestamp offset. For example, if the timestamp offset is -100 and 31525 // we buffered times 0 to 100 with real times of 100 to 200, even though current 31526 // time will be between 0 and 100, the native media source may report the duration 31527 // as 200. However, since we report duration separate from the media source (as 31528 // Infinity), and as long as the native media source duration value is greater than 31529 // our reported seekable range, seeks will work as expected. The large number as 31530 // duration for live is actually a strategy used by some players to work around the 31531 // issue of live seekable ranges cited above. 31532 31533 31534 if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) { 31535 this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1)); 31536 } 31537 31538 return; 31539 } 31540 31541 const buffered = this.tech_.buffered(); 31542 let duration = Vhs$1.Playlist.duration(this.mainPlaylistLoader_.media()); 31543 31544 if (buffered.length > 0) { 31545 duration = Math.max(duration, buffered.end(buffered.length - 1)); 31546 } 31547 31548 if (this.mediaSource.duration !== duration) { 31549 this.sourceUpdater_.setDuration(duration); 31550 } 31551 } 31552 /** 31553 * dispose of the PlaylistController and everything 31554 * that it controls 31555 */ 31556 31557 31558 dispose() { 31559 this.trigger('dispose'); 31560 this.decrypter_.terminate(); 31561 this.mainPlaylistLoader_.dispose(); 31562 this.mainSegmentLoader_.dispose(); 31563 this.contentSteeringController_.dispose(); 31564 this.keyStatusMap_.clear(); 31565 31566 if (this.loadOnPlay_) { 31567 this.tech_.off('play', this.loadOnPlay_); 31568 } 31569 31570 ['AUDIO', 'SUBTITLES'].forEach(type => { 31571 const groups = this.mediaTypes_[type].groups; 31572 31573 for (const id in groups) { 31574 groups[id].forEach(group => { 31575 if (group.playlistLoader) { 31576 group.playlistLoader.dispose(); 31577 } 31578 }); 31579 } 31580 }); 31581 this.audioSegmentLoader_.dispose(); 31582 this.subtitleSegmentLoader_.dispose(); 31583 this.sourceUpdater_.dispose(); 31584 this.timelineChangeController_.dispose(); 31585 this.stopABRTimer_(); 31586 31587 if (this.updateDuration_) { 31588 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 31589 } 31590 31591 this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 31592 31593 this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_); 31594 this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_); 31595 this.off(); 31596 } 31597 /** 31598 * return the main playlist object if we have one 31599 * 31600 * @return {Object} the main playlist object that we parsed 31601 */ 31602 31603 31604 main() { 31605 return this.mainPlaylistLoader_.main; 31606 } 31607 /** 31608 * return the currently selected playlist 31609 * 31610 * @return {Object} the currently selected playlist object that we parsed 31611 */ 31612 31613 31614 media() { 31615 // playlist loader will not return media if it has not been fully loaded 31616 return this.mainPlaylistLoader_.media() || this.initialMedia_; 31617 } 31618 31619 areMediaTypesKnown_() { 31620 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 31621 const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_(); // if we are not using an audio loader, then we have audio media info 31622 // otherwise check on the segment loader. 31623 31624 const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_(); // one or both loaders has not loaded sufficently to get codecs 31625 31626 if (!hasMainMediaInfo || !hasAudioMediaInfo) { 31627 return false; 31628 } 31629 31630 return true; 31631 }
vendor: 6,231 bytes, lines 31631-31803
31631 31632 31633 getCodecsOrExclude_() { 31634 const media = { 31635 main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {}, 31636 audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {} 31637 }; 31638 const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); // set "main" media equal to video 31639 31640 media.video = media.main; 31641 const playlistCodecs = codecsForPlaylist(this.main(), playlist); 31642 const codecs = {}; 31643 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 31644 31645 if (media.main.hasVideo) { 31646 codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC; 31647 } 31648 31649 if (media.main.isMuxed) { 31650 codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`; 31651 } 31652 31653 if (media.main.hasAudio && !media.main.isMuxed || media.audio.hasAudio || usingAudioLoader) { 31654 codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC; // set audio isFmp4 so we use the correct "supports" function below 31655 31656 media.audio.isFmp4 = media.main.hasAudio && !media.main.isMuxed ? media.main.isFmp4 : media.audio.isFmp4; 31657 } // no codecs, no playback. 31658 31659 31660 if (!codecs.audio && !codecs.video) { 31661 this.excludePlaylist({ 31662 playlistToExclude: playlist, 31663 error: { 31664 message: 'Could not determine codecs for playlist.' 31665 }, 31666 playlistExclusionDuration: Infinity 31667 }); 31668 return; 31669 } // fmp4 relies on browser support, while ts relies on muxer support 31670 31671 31672 const supportFunction = (isFmp4, codec) => isFmp4 ? browserSupportsCodec(codec) : muxerSupportsCodec(codec); 31673 31674 const unsupportedCodecs = {}; 31675 let unsupportedAudio; 31676 ['video', 'audio'].forEach(function (type) { 31677 if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) { 31678 const supporter = media[type].isFmp4 ? 'browser' : 'muxer'; 31679 unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || []; 31680 unsupportedCodecs[supporter].push(codecs[type]); 31681 31682 if (type === 'audio') { 31683 unsupportedAudio = supporter; 31684 } 31685 } 31686 }); 31687 31688 if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) { 31689 const audioGroup = playlist.attributes.AUDIO; 31690 this.main().playlists.forEach(variant => { 31691 const variantAudioGroup = variant.attributes && variant.attributes.AUDIO; 31692 31693 if (variantAudioGroup === audioGroup && variant !== playlist) { 31694 variant.excludeUntil = Infinity; 31695 } 31696 }); 31697 this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`); 31698 } // if we have any unsupported codecs exclude this playlist. 31699 31700 31701 if (Object.keys(unsupportedCodecs).length) { 31702 const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => { 31703 if (acc) { 31704 acc += ', '; 31705 } 31706 31707 acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`; 31708 return acc; 31709 }, '') + '.'; 31710 this.excludePlaylist({ 31711 playlistToExclude: playlist, 31712 error: { 31713 internal: true, 31714 message 31715 }, 31716 playlistExclusionDuration: Infinity 31717 }); 31718 return; 31719 } // check if codec switching is happening 31720 31721 31722 if (this.sourceUpdater_.hasCreatedSourceBuffers() && !this.sourceUpdater_.canChangeType()) { 31723 const switchMessages = []; 31724 ['video', 'audio'].forEach(type => { 31725 const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type; 31726 const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type; 31727 31728 if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) { 31729 switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`); 31730 } 31731 }); 31732 31733 if (switchMessages.length) { 31734 this.excludePlaylist({ 31735 playlistToExclude: playlist, 31736 error: { 31737 message: `Codec switching not supported: ${switchMessages.join(', ')}.`, 31738 internal: true 31739 }, 31740 playlistExclusionDuration: Infinity 31741 }); 31742 return; 31743 } 31744 } // TODO: when using the muxer shouldn't we just return 31745 // the codecs that the muxer outputs? 31746 31747 31748 return codecs; 31749 } 31750 /** 31751 * Create source buffers and exlude any incompatible renditions. 31752 * 31753 * @private 31754 */ 31755 31756 31757 tryToCreateSourceBuffers_() { 31758 // media source is not ready yet or sourceBuffers are already 31759 // created. 31760 if (this.mediaSource.readyState !== 'open' || this.sourceUpdater_.hasCreatedSourceBuffers()) { 31761 return; 31762 } 31763 31764 if (!this.areMediaTypesKnown_()) { 31765 return; 31766 } 31767 31768 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 31769 31770 if (!codecs) { 31771 return; 31772 } 31773 31774 this.sourceUpdater_.createSourceBuffers(codecs); 31775 const codecString = [codecs.video, codecs.audio].filter(Boolean).join(','); 31776 this.excludeIncompatibleVariants_(codecString); 31777 } 31778 /** 31779 * Excludes playlists with codecs that are unsupported by the muxer and browser. 31780 */ 31781 31782 31783 excludeUnsupportedVariants_() { 31784 const playlists = this.main().playlists; 31785 const ids = []; // TODO: why don't we have a property to loop through all 31786 // playlist? Why did we ever mix indexes and keys? 31787 31788 Object.keys(playlists).forEach(key => { 31789 const variant = playlists[key]; // check if we already processed this playlist. 31790 31791 if (ids.indexOf(variant.id) !== -1) { 31792 return; 31793 } 31794 31795 ids.push(variant.id); 31796 const codecs = codecsForPlaylist(this.main, variant); 31797 const unsupported = []; 31798 31799 if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio)) { 31800 unsupported.push(`audio codec ${codecs.audio}`); 31801 } 31802 31803 if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video
vendor: 20,339 bytes, lines 31803-32344
31803)) { 31804 unsupported.push(`video codec ${codecs.video}`); 31805 } 31806 31807 if (codecs.text && codecs.text === 'stpp.ttml.im1t') { 31808 unsupported.push(`text codec ${codecs.text}`); 31809 } 31810 31811 if (unsupported.length) { 31812 variant.excludeUntil = Infinity; 31813 this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`); 31814 } 31815 }); 31816 } 31817 /** 31818 * Exclude playlists that are known to be codec or 31819 * stream-incompatible with the SourceBuffer configuration. For 31820 * instance, Media Source Extensions would cause the video element to 31821 * stall waiting for video data if you switched from a variant with 31822 * video and audio to an audio-only one. 31823 * 31824 * @param {Object} media a media playlist compatible with the current 31825 * set of SourceBuffers. Variants in the current main playlist that 31826 * do not appear to have compatible codec or stream configurations 31827 * will be excluded from the default playlist selection algorithm 31828 * indefinitely. 31829 * @private 31830 */ 31831 31832 31833 excludeIncompatibleVariants_(codecString) { 31834 const ids = []; 31835 const playlists = this.main().playlists; 31836 const codecs = unwrapCodecList(parseCodecs(codecString)); 31837 const codecCount_ = codecCount(codecs); 31838 const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null; 31839 const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null; 31840 Object.keys(playlists).forEach(key => { 31841 const variant = playlists[key]; // check if we already processed this playlist. 31842 // or it if it is already excluded forever. 31843 31844 if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) { 31845 return; 31846 } 31847 31848 ids.push(variant.id); 31849 const exclusionReasons = []; // get codecs from the playlist for this variant 31850 31851 const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant); 31852 const variantCodecCount = codecCount(variantCodecs); // if no codecs are listed, we cannot determine that this 31853 // variant is incompatible. Wait for mux.js to probe 31854 31855 if (!variantCodecs.audio && !variantCodecs.video) { 31856 return; 31857 } // TODO: we can support this by removing the 31858 // old media source and creating a new one, but it will take some work. 31859 // The number of streams cannot change 31860 31861 31862 if (variantCodecCount !== codecCount_) { 31863 exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`); 31864 } // only exclude playlists by codec change, if codecs cannot switch 31865 // during playback. 31866 31867 31868 if (!this.sourceUpdater_.canChangeType()) { 31869 const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null; 31870 const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null; // the video codec cannot change 31871 31872 if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) { 31873 exclusionReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`); 31874 } // the audio codec cannot change 31875 31876 31877 if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) { 31878 exclusionReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`); 31879 } 31880 } 31881 31882 if (exclusionReasons.length) { 31883 variant.excludeUntil = Infinity; 31884 this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`); 31885 } 31886 }); 31887 } 31888 31889 updateAdCues_(media) { 31890 let offset = 0; 31891 const seekable = this.seekable(); 31892 31893 if (seekable.length) { 31894 offset = seekable.start(0); 31895 } 31896 31897 updateAdCues(media, this.cueTagsTrack_, offset); 31898 } 31899 /** 31900 * Calculates the desired forward buffer length based on current time 31901 * 31902 * @return {number} Desired forward buffer length in seconds 31903 */ 31904 31905 31906 goalBufferLength() { 31907 const currentTime = this.tech_.currentTime(); 31908 const initial = Config.GOAL_BUFFER_LENGTH; 31909 const rate = Config.GOAL_BUFFER_LENGTH_RATE; 31910 const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 31911 return Math.min(initial + currentTime * rate, max); 31912 } 31913 /** 31914 * Calculates the desired buffer low water line based on current time 31915 * 31916 * @return {number} Desired buffer low water line in seconds 31917 */ 31918 31919 31920 bufferLowWaterLine() { 31921 const currentTime = this.tech_.currentTime(); 31922 const initial = Config.BUFFER_LOW_WATER_LINE; 31923 const rate = Config.BUFFER_LOW_WATER_LINE_RATE; 31924 const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 31925 const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE); 31926 return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max); 31927 } 31928 31929 bufferHighWaterLine() { 31930 return Config.BUFFER_HIGH_WATER_LINE; 31931 } 31932 31933 addDateRangesToTextTrack_(dateRanges) { 31934 createMetadataTrackIfNotExists(this.inbandTextTracks_, 'com.apple.streaming', this.tech_); 31935 addDateRangeMetadata({ 31936 inbandTextTracks: this.inbandTextTracks_, 31937 dateRanges 31938 }); 31939 } 31940 31941 addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) { 31942 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 31943 // audio/video source with a metadata track, and an alt audio with a metadata track. 31944 // However, this probably won't happen, and if it does it can be handled then. 31945 31946 createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_); 31947 addMetadata({ 31948 inbandTextTracks: this.inbandTextTracks_, 31949 metadataArray, 31950 timestampOffset, 31951 videoDuration 31952 }); 31953 } 31954 /** 31955 * Utility for getting the pathway or service location from an HLS or DASH playlist. 31956 * 31957 * @param {Object} playlist for getting pathway from. 31958 * @return the pathway attribute of a playlist 31959 */ 31960 31961 31962 pathwayAttribute_(playlist) { 31963 return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation; 31964 } 31965 /** 31966 * Initialize available pathways and apply the tag properties. 31967 */ 31968 31969 31970 initContentSteeringController_() { 31971 const main = this.main(); 31972 31973 if (!main.contentSteering) { 31974 return; 31975 } 31976 31977 for (const playlist of main.playlists) { 31978 this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist)); 31979 } 31980 31981 this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering); // request the steering manifest immediately if queryBeforeStart is set. 31982 31983 if (this.contentSteeringController_.queryBeforeStart) { 31984 // When queryBeforeStart is true, initial request should omit steering parameters. 31985 this.contentSteeringController_.requestSteeringManifest(true); 31986 return; 31987 } // otherwise start content steering after playback starts 31988 31989 31990 this.tech_.one('canplay', () => { 31991 this.contentSteeringController_.requestSteeringManifest(); 31992 }); 31993 } 31994 /** 31995 * Reset the content steering controller and re-init. 31996 */ 31997 31998 31999 resetContentSteeringController_() { 32000 this.contentSteeringController_.clearAvailablePathways(); 32001 this.contentSteeringController_.dispose(); 32002 this.initContentSteeringController_(); 32003 } 32004 /** 32005 * Attaches the listeners for content steering. 32006 */ 32007 32008 32009 attachContentSteeringListeners_() { 32010 this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this)); 32011 32012 if (this.sourceType_ === 'dash') { 32013 this.mainPlaylistLoader_.on('loadedplaylist', () => { 32014 const main = this.main(); // check if steering tag or pathways changed. 32015 32016 const didDashTagChange = this.contentSteeringController_.didDASHTagChange(main.uri, main.contentSteering); 32017 32018 const didPathwaysChange = () => { 32019 const availablePathways = this.contentSteeringController_.getAvailablePathways(); 32020 const newPathways = []; 32021 32022 for (const playlist of main.playlists) { 32023 const serviceLocation = playlist.attributes.serviceLocation; 32024 32025 if (serviceLocation) { 32026 newPathways.push(serviceLocation); 32027 32028 if (!availablePathways.has(serviceLocation)) { 32029 return true; 32030 } 32031 } 32032 } // If we have no new serviceLocations and previously had availablePathways 32033 32034 32035 if (!newPathways.length && availablePathways.size) { 32036 return true; 32037 } 32038 32039 return false; 32040 }; 32041 32042 if (didDashTagChange || didPathwaysChange()) { 32043 this.resetContentSteeringController_(); 32044 } 32045 }); 32046 } 32047 } 32048 /** 32049 * Simple exclude and change playlist logic for content steering. 32050 */ 32051 32052 32053 excludeThenChangePathway_() { 32054 const currentPathway = this.contentSteeringController_.getPathway(); 32055 32056 if (!currentPathway) { 32057 return; 32058 } 32059 32060 this.handlePathwayClones_(); 32061 const main = this.main(); 32062 const playlists = main.playlists; 32063 const ids = new Set(); 32064 let didEnablePlaylists = false; 32065 Object.keys(playlists).forEach(key => { 32066 const variant = playlists[key]; 32067 const pathwayId = this.pathwayAttribute_(variant); 32068 const differentPathwayId = pathwayId && currentPathway !== pathwayId; 32069 const steeringExclusion = variant.excludeUntil === Infinity && variant.lastExcludeReason_ === 'content-steering'; 32070 32071 if (steeringExclusion && !differentPathwayId) { 32072 delete variant.excludeUntil; 32073 delete variant.lastExcludeReason_; 32074 didEnablePlaylists = true; 32075 } 32076 32077 const noExcludeUntil = !variant.excludeUntil && variant.excludeUntil !== Infinity; 32078 const shouldExclude = !ids.has(variant.id) && differentPathwayId && noExcludeUntil; 32079 32080 if (!shouldExclude) { 32081 return; 32082 } 32083 32084 ids.add(variant.id); 32085 variant.excludeUntil = Infinity; 32086 variant.lastExcludeReason_ = 'content-steering'; // TODO: kind of spammy, maybe move this. 32087 32088 this.logger_(`excluding ${variant.id} for ${variant.lastExcludeReason_}`); 32089 }); 32090 32091 if (this.contentSteeringController_.manifestType_ === 'DASH') { 32092 Object.keys(this.mediaTypes_).forEach(key => { 32093 const type = this.mediaTypes_[key]; 32094 32095 if (type.activePlaylistLoader) { 32096 const currentPlaylist = type.activePlaylistLoader.media_; // Check if the current media playlist matches the current CDN 32097 32098 if (currentPlaylist && currentPlaylist.attributes.serviceLocation !== currentPathway) { 32099 didEnablePlaylists = true; 32100 } 32101 } 32102 }); 32103 } 32104 32105 if (didEnablePlaylists) { 32106 this.changeSegmentPathway_(); 32107 } 32108 } 32109 /** 32110 * Add, update, or delete playlists and media groups for 32111 * the pathway clones for HLS Content Steering. 32112 * 32113 * See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 32114 * 32115 * NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and 32116 * `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or 32117 * `STABLE-RENDITION-ID` values. 32118 */ 32119 32120 32121 handlePathwayClones_() { 32122 const main = this.main(); 32123 const playlists = main.playlists; 32124 const currentPathwayClones = this.contentSteeringController_.currentPathwayClones; 32125 const nextPathwayClones = this.contentSteeringController_.nextPathwayClones; 32126 const hasClones = currentPathwayClones && currentPathwayClones.size || nextPathwayClones && nextPathwayClones.size; 32127 32128 if (!hasClones) { 32129 return; 32130 } 32131 32132 for (const [id, clone] of currentPathwayClones.entries()) { 32133 const newClone = nextPathwayClones.get(id); // Delete the old pathway clone. 32134 32135 if (!newClone) { 32136 this.mainPlaylistLoader_.updateOrDeleteClone(clone); 32137 this.contentSteeringController_.excludePathway(id); 32138 } 32139 } 32140 32141 for (const [id, clone] of nextPathwayClones.entries()) { 32142 const oldClone = currentPathwayClones.get(id); // Create a new pathway if it is a new pathway clone object. 32143 32144 if (!oldClone) { 32145 const playlistsToClone = playlists.filter(p => { 32146 return p.attributes['PATHWAY-ID'] === clone['BASE-ID']; 32147 }); 32148 playlistsToClone.forEach(p => { 32149 this.mainPlaylistLoader_.addClonePathway(clone, p); 32150 }); 32151 this.contentSteeringController_.addAvailablePathway(id); 32152 continue; 32153 } // There have not been changes to the pathway clone object, so skip. 32154 32155 32156 if (this.equalPathwayClones_(oldClone, clone)) { 32157 continue; 32158 } // Update a preexisting cloned pathway. 32159 // True is set for the update flag. 32160 32161 32162 this.mainPlaylistLoader_.updateOrDeleteClone(clone, true); 32163 this.contentSteeringController_.addAvailablePathway(id); 32164 } // Deep copy contents of next to current pathways. 32165 32166 32167 this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones]))); 32168 } 32169 /** 32170 * Determines whether two pathway clone objects are equivalent. 32171 * 32172 * @param {Object} a The first pathway clone object. 32173 * @param {Object} b The second pathway clone object. 32174 * @return {boolean} True if the pathway clone objects are equal, false otherwise. 32175 */ 32176 32177 32178 equalPathwayClones_(a, b) { 32179 if (a['BASE-ID'] !== b['BASE-ID'] || a.ID !== b.ID || a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST) { 32180 return false; 32181 } 32182 32183 const aParams = a['URI-REPLACEMENT'].PARAMS; 32184 const bParams = b['URI-REPLACEMENT'].PARAMS; // We need to iterate through both lists of params because one could be 32185 // missing a parameter that the other has. 32186 32187 for (const p in aParams) { 32188 if (aParams[p] !== bParams[p]) { 32189 return false; 32190 } 32191 } 32192 32193 for (const p in bParams) { 32194 if (aParams[p] !== bParams[p]) { 32195 return false; 32196 } 32197 } 32198 32199 return true; 32200 } 32201 /** 32202 * Changes the current playlists for audio, video and subtitles after a new pathway 32203 * is chosen from content steering. 32204 */ 32205 32206 32207 changeSegmentPathway_() { 32208 const nextPlaylist = this.selectPlaylist(); 32209 this.pauseLoading(); // Switch audio and text track playlists if necessary in DASH 32210 32211 if (this.contentSteeringController_.manifestType_ === 'DASH') { 32212 this.switchMediaForDASHContentSteering_(); 32213 } 32214 32215 this.switchMedia_(nextPlaylist, 'content-steering'); 32216 } 32217 /** 32218 * Iterates through playlists and check their keyId set and compare with the 32219 * keyStatusMap, only enable playlists that have a usable key. If the playlist 32220 * has no keyId leave it enabled by default. 32221 */ 32222 32223 32224 excludeNonUsablePlaylistsByKeyId_() { 32225 if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) { 32226 return; 32227 } 32228 32229 let nonUsableKeyStatusCount = 0; 32230 const NON_USABLE = 'non-usable'; 32231 this.mainPlaylistLoader_.main.playlists.forEach(playlist => { 32232 const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); // If the playlist doesn't have keyIDs lets not exclude it. 32233 32234 if (!keyIdSet || !keyIdSet.size) { 32235 return; 32236 } 32237 32238 keyIdSet.forEach(key => { 32239 const USABLE = 'usable'; 32240 const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE; 32241 const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity; 32242 32243 if (!hasUsableKeyStatus) { 32244 // Only exclude playlists that haven't already been excluded as non-usable. 32245 if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) { 32246 playlist.excludeUntil = Infinity; 32247 playlist.lastExcludeReason_ = NON_USABLE; 32248 this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`); 32249 } // count all nonUsableKeyStatus 32250 32251 32252 nonUsableKeyStatusCount++; 32253 } else if (hasUsableKeyStatus && nonUsableExclusion) { 32254 delete playlist.excludeUntil; 32255 delete playlist.lastExcludeReason_; 32256 this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`); 32257 } 32258 }); 32259 }); // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe. 32260 32261 if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) { 32262 this.mainPlaylistLoader_.main.playlists.forEach(playlist => { 32263 const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720; 32264 const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE; 32265 32266 if (isNonHD && excludedForNonUsableKey) { 32267 // Only delete the excludeUntil so we don't try and re-exclude these playlists. 32268 delete playlist.excludeUntil; 32269 videojs__default["default"].log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`); 32270 } 32271 }); 32272 } 32273 } 32274 /** 32275 * Adds a keystatus to the keystatus map, tries to convert to string if necessary. 32276 * 32277 * @param {any} keyId the keyId to add a status for 32278 * @param {string} status the status of the keyId 32279 */ 32280 32281 32282 addKeyStatus_(keyId, status) { 32283 const isString = typeof keyId === 'string'; 32284 const keyIdHexString = isString ? keyId : bufferToHexString(keyId); 32285 const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase(); 32286 this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`); 32287 this.keyStatusMap_.set(formattedKeyIdString, status); 32288 } 32289 /** 32290 * Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists. 32291 * 32292 * @param {any} keyId the keyId from the keystatuschange event 32293 * @param {string} status the key status string 32294 */ 32295 32296 32297 updatePlaylistByKeyStatus(keyId, status) { 32298 this.addKeyStatus_(keyId, status); 32299 32300 if (!this.waitingForFastQualityPlaylistReceived_) { 32301 this.excludeNonUsableThenChangePlaylist_(); 32302 } // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated. 32303 32304 32305 this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); 32306 this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); 32307 } 32308 32309 excludeNonUsableThenChangePlaylist_() { 32310 this.excludeNonUsablePlaylistsByKeyId_(); 32311 this.fastQualityChange_(); 32312 } 32313 32314 } 32315 32316 /** 32317 * Returns a function that acts as the Enable/disable playlist function. 32318 * 32319 * @param {PlaylistLoader} loader - The main playlist loader 32320 * @param {string} playlistID - id of the playlist 32321 * @param {Function} changePlaylistFn - A function to be called after a 32322 * playlist's enabled-state has been changed. Will NOT be called if a 32323 * playlist's enabled-state is unchanged 32324 * @param {boolean=} enable - Value to set the playlist enabled-state to 32325 * or if undefined returns the current enabled-state for the playlist 32326 * @return {Function} Function for setting/getting enabled 32327 */ 32328 32329 const enableFunction = (loader, playlistID, changePlaylistFn) => enable => { 32330 const playlist = loader.main.playlists[playlistID]; 32331 const incompatible = isIncompatible(playlist); 32332 const currentlyEnabled = isEnabled(playlist); 32333 32334 if (typeof enable === 'undefined') { 32335 return currentlyEnabled; 32336 } 32337 32338 if (enable) { 32339 delete playlist.disabled; 32340 } else { 32341 playlist.disabled = true; 32342 } 32343 32344
vendor: 12,300 bytes, lines 32344-32673
32344if (enable !== currentlyEnabled && !incompatible) { 32345 // Ensure the outside world knows about our changes 32346 changePlaylistFn(); 32347 32348 if (enable) { 32349 loader.trigger('renditionenabled'); 32350 } else { 32351 loader.trigger('renditiondisabled'); 32352 } 32353 } 32354 32355 return enable; 32356 }; 32357 /** 32358 * The representation object encapsulates the publicly visible information 32359 * in a media playlist along with a setter/getter-type function (enabled) 32360 * for changing the enabled-state of a particular playlist entry 32361 * 32362 * @class Representation 32363 */ 32364 32365 32366 class Representation { 32367 constructor(vhsHandler, playlist, id) { 32368 const { 32369 playlistController_: pc 32370 } = vhsHandler; 32371 const qualityChangeFunction = pc.fastQualityChange_.bind(pc); // some playlist attributes are optional 32372 32373 if (playlist.attributes) { 32374 const resolution = playlist.attributes.RESOLUTION; 32375 this.width = resolution && resolution.width; 32376 this.height = resolution && resolution.height; 32377 this.bandwidth = playlist.attributes.BANDWIDTH; 32378 this.frameRate = playlist.attributes['FRAME-RATE']; 32379 } 32380 32381 this.codecs = codecsForPlaylist(pc.main(), playlist); 32382 this.playlist = playlist; // The id is simply the ordinality of the media playlist 32383 // within the main playlist 32384 32385 this.id = id; // Partially-apply the enableFunction to create a playlist- 32386 // specific variant 32387 32388 this.enabled = enableFunction(vhsHandler.playlists, playlist.id, qualityChangeFunction); 32389 } 32390 32391 } 32392 /** 32393 * A mixin function that adds the `representations` api to an instance 32394 * of the VhsHandler class 32395 * 32396 * @param {VhsHandler} vhsHandler - An instance of VhsHandler to add the 32397 * representation API into 32398 */ 32399 32400 32401 const renditionSelectionMixin = function (vhsHandler) { 32402 // Add a single API-specific function to the VhsHandler instance 32403 vhsHandler.representations = () => { 32404 const main = vhsHandler.playlistController_.main(); 32405 const playlists = isAudioOnly(main) ? vhsHandler.playlistController_.getAudioTrackPlaylists_() : main.playlists; 32406 32407 if (!playlists) { 32408 return []; 32409 } 32410 32411 return playlists.filter(media => !isIncompatible(media)).map((e, i) => new Representation(vhsHandler, e, e.id)); 32412 }; 32413 }; 32414 32415 /** 32416 * @file playback-watcher.js 32417 * 32418 * Playback starts, and now my watch begins. It shall not end until my death. I shall 32419 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 32420 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 32421 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 32422 * my life and honor to the Playback Watch, for this Player and all the Players to come. 32423 */ 32424 32425 const timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 32426 /** 32427 * @class PlaybackWatcher 32428 */ 32429 32430 class PlaybackWatcher { 32431 /** 32432 * Represents an PlaybackWatcher object. 32433 * 32434 * @class 32435 * @param {Object} options an object that includes the tech and settings 32436 */ 32437 constructor(options) { 32438 this.playlistController_ = options.playlistController; 32439 this.tech_ = options.tech; 32440 this.seekable = options.seekable; 32441 this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow; 32442 this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta; 32443 this.media = options.media; 32444 this.consecutiveUpdates = 0; 32445 this.lastRecordedTime = null; 32446 this.checkCurrentTimeTimeout_ = null; 32447 this.logger_ = logger('PlaybackWatcher'); 32448 this.logger_('initialize'); 32449 32450 const playHandler = () => this.monitorCurrentTime_(); 32451 32452 const canPlayHandler = () => this.monitorCurrentTime_(); 32453 32454 const waitingHandler = () => this.techWaiting_(); 32455 32456 const cancelTimerHandler = () => this.resetTimeUpdate_(); 32457 32458 const pc = this.playlistController_; 32459 const loaderTypes = ['main', 'subtitle', 'audio']; 32460 const loaderChecks = {}; 32461 loaderTypes.forEach(type => { 32462 loaderChecks[type] = { 32463 reset: () => this.resetSegmentDownloads_(type), 32464 updateend: () => this.checkSegmentDownloads_(type) 32465 }; 32466 pc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend); // If a rendition switch happens during a playback stall where the buffer 32467 // isn't changing we want to reset. We cannot assume that the new rendition 32468 // will also be stalled, until after new appends. 32469 32470 pc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset); // Playback stalls should not be detected right after seeking. 32471 // This prevents one segment playlists (single vtt or single segment content) 32472 // from being detected as stalling. As the buffer will not change in those cases, since 32473 // the buffer is the entire video duration. 32474 32475 this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset); 32476 }); 32477 /** 32478 * We check if a seek was into a gap through the following steps: 32479 * 1. We get a seeking event and we do not get a seeked event. This means that 32480 * a seek was attempted but not completed. 32481 * 2. We run `fixesBadSeeks_` on segment loader appends. This means that we already 32482 * removed everything from our buffer and appended a segment, and should be ready 32483 * to check for gaps. 32484 */ 32485 32486 const setSeekingHandlers = fn => { 32487 ['main', 'audio'].forEach(type => { 32488 pc[`${type}SegmentLoader_`][fn]('appended', this.seekingAppendCheck_); 32489 }); 32490 }; 32491 32492 this.seekingAppendCheck_ = () => { 32493 if (this.fixesBadSeeks_()) { 32494 this.consecutiveUpdates = 0; 32495 this.lastRecordedTime = this.tech_.currentTime(); 32496 setSeekingHandlers('off'); 32497 } 32498 }; 32499 32500 this.clearSeekingAppendCheck_ = () => setSeekingHandlers('off'); 32501 32502 this.watchForBadSeeking_ = () => { 32503 this.clearSeekingAppendCheck_(); 32504 setSeekingHandlers('on'); 32505 }; 32506 32507 this.tech_.on('seeked', this.clearSeekingAppendCheck_); 32508 this.tech_.on('seeking', this.watchForBadSeeking_); 32509 this.tech_.on('waiting', waitingHandler); 32510 this.tech_.on(timerCancelEvents, cancelTimerHandler); 32511 this.tech_.on('canplay', canPlayHandler); 32512 /* 32513 An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case 32514 is surfaced in one of two ways: 32515 1) The `waiting` event is fired before the player has buffered content, making it impossible 32516 to find or skip the gap. The `waiting` event is followed by a `play` event. On first play 32517 we can check if playback is stalled due to a gap, and skip the gap if necessary. 32518 2) A source with a gap at the beginning of the stream is loaded programatically while the player 32519 is in a playing state. To catch this case, it's important that our one-time play listener is setup 32520 even if the player is in a playing state 32521 */ 32522 32523 this.tech_.one('play', playHandler); // Define the dispose function to clean up our events 32524 32525 this.dispose = () => { 32526 this.clearSeekingAppendCheck_(); 32527 this.logger_('dispose'); 32528 this.tech_.off('waiting', waitingHandler); 32529 this.tech_.off(timerCancelEvents, cancelTimerHandler); 32530 this.tech_.off('canplay', canPlayHandler); 32531 this.tech_.off('play', playHandler); 32532 this.tech_.off('seeking', this.watchForBadSeeking_); 32533 this.tech_.off('seeked', this.clearSeekingAppendCheck_); 32534 loaderTypes.forEach(type => { 32535 pc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend); 32536 pc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset); 32537 this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset); 32538 }); 32539 32540 if (this.checkCurrentTimeTimeout_) { 32541 window.clearTimeout(this.checkCurrentTimeTimeout_); 32542 } 32543 32544 this.resetTimeUpdate_(); 32545 }; 32546 } 32547 /** 32548 * Periodically check current time to see if playback stopped 32549 * 32550 * @private 32551 */ 32552 32553 32554 monitorCurrentTime_() { 32555 this.checkCurrentTime_(); 32556 32557 if (this.checkCurrentTimeTimeout_) { 32558 window.clearTimeout(this.checkCurrentTimeTimeout_); 32559 } // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 32560 32561 32562 this.checkCurrentTimeTimeout_ = window.setTimeout(this.monitorCurrentTime_.bind(this), 250); 32563 } 32564 /** 32565 * Reset stalled download stats for a specific type of loader 32566 * 32567 * @param {string} type 32568 * The segment loader type to check. 32569 * 32570 * @listens SegmentLoader#playlistupdate 32571 * @listens Tech#seeking 32572 * @listens Tech#seeked 32573 */ 32574 32575 32576 resetSegmentDownloads_(type) { 32577 const loader = this.playlistController_[`${type}SegmentLoader_`]; 32578 32579 if (this[`${type}StalledDownloads_`] > 0) { 32580 this.logger_(`resetting possible stalled download count for ${type} loader`); 32581 } 32582 32583 this[`${type}StalledDownloads_`] = 0; 32584 this[`${type}Buffered_`] = loader.buffered_(); 32585 } 32586 /** 32587 * Checks on every segment `appendsdone` to see 32588 * if segment appends are making progress. If they are not 32589 * and we are still downloading bytes. We exclude the playlist. 32590 * 32591 * @param {string} type 32592 * The segment loader type to check. 32593 * 32594 * @listens SegmentLoader#appendsdone 32595 */ 32596 32597 32598 checkSegmentDownloads_(type) { 32599 const pc = this.playlistController_; 32600 const loader = pc[`${type}SegmentLoader_`]; 32601 const buffered = loader.buffered_(); 32602 const isBufferedDifferent = isRangeDifferent(this[`${type}Buffered_`], buffered); 32603 this[`${type}Buffered_`] = buffered; // if another watcher is going to fix the issue or 32604 // the buffered value for this loader changed 32605 // appends are working 32606 32607 if (isBufferedDifferent) { 32608 this.resetSegmentDownloads_(type); 32609 return; 32610 } 32611 32612 this[`${type}StalledDownloads_`]++; 32613 this.logger_(`found #${this[`${type}StalledDownloads_`]} ${type} appends that did not increase buffer (possible stalled download)`, { 32614 playlistId: loader.playlist_ && loader.playlist_.id, 32615 buffered: timeRangesToArray(buffered) 32616 }); // after 10 possibly stalled appends with no reset, exclude 32617 32618 if (this[`${type}StalledDownloads_`] < 10) { 32619 return; 32620 } 32621 32622 this.logger_(`${type} loader stalled download exclusion`); 32623 this.resetSegmentDownloads_(type); 32624 this.tech_.trigger({ 32625 type: 'usage', 32626 name: `vhs-${type}-download-exclusion` 32627 }); 32628 32629 if (type === 'subtitle') { 32630 return; 32631 } // TODO: should we exclude audio tracks rather than main tracks 32632 // when type is audio? 32633 32634 32635 pc.excludePlaylist({ 32636 error: { 32637 message: `Excessive ${type} segment downloading detected.` 32638 }, 32639 playlistExclusionDuration: Infinity 32640 }); 32641 } 32642 /** 32643 * The purpose of this function is to emulate the "waiting" event on 32644 * browsers that do not emit it when they are waiting for more 32645 * data to continue playback 32646 * 32647 * @private 32648 */ 32649 32650 32651 checkCurrentTime_() { 32652 if (this.tech_.paused() || this.tech_.seeking()) { 32653 return; 32654 } 32655 32656 const currentTime = this.tech_.currentTime(); 32657 const buffered = this.tech_.buffered(); 32658 32659 if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 32660 // If current time is at the end of the final buffered region, then any playback 32661 // stall is most likely caused by buffering in a low bandwidth environment. The tech 32662 // should fire a `waiting` event in this scenario, but due to browser and tech 32663 // inconsistencies. Calling `techWaiting_` here allows us to simulate 32664 // responding to a native `waiting` event when the tech fails to emit one. 32665 return this.techWaiting_(); 32666 } 32667 32668 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 32669 this.consecutiveUpdates++; 32670 this.waiting_(); 32671 } else if (currentTime === this.lastRecordedTime) { 32672 this.consecutiveUpdates++; 32673 } else {
vendor: 11,147 bytes, lines 32673-32969
32673 32674 this.consecutiveUpdates = 0; 32675 this.lastRecordedTime = currentTime; 32676 } 32677 } 32678 /** 32679 * Resets the 'timeupdate' mechanism designed to detect that we are stalled 32680 * 32681 * @private 32682 */ 32683 32684 32685 resetTimeUpdate_() { 32686 this.consecutiveUpdates = 0; 32687 } 32688 /** 32689 * Fixes situations where there's a bad seek 32690 * 32691 * @return {boolean} whether an action was taken to fix the seek 32692 * @private 32693 */ 32694 32695 32696 fixesBadSeeks_() { 32697 const seeking = this.tech_.seeking(); 32698 32699 if (!seeking) { 32700 return false; 32701 } // TODO: It's possible that these seekable checks should be moved out of this function 32702 // and into a function that runs on seekablechange. It's also possible that we only need 32703 // afterSeekableWindow as the buffered check at the bottom is good enough to handle before 32704 // seekable range. 32705 32706 32707 const seekable = this.seekable(); 32708 const currentTime = this.tech_.currentTime(); 32709 const isAfterSeekableRange = this.afterSeekableWindow_(seekable, currentTime, this.media(), this.allowSeeksWithinUnsafeLiveWindow); 32710 let seekTo; 32711 32712 if (isAfterSeekableRange) { 32713 const seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 32714 32715 seekTo = seekableEnd; 32716 } 32717 32718 if (this.beforeSeekableWindow_(seekable, currentTime)) { 32719 const seekableStart = seekable.start(0); // sync to the beginning of the live window 32720 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 32721 32722 seekTo = seekableStart + ( // if the playlist is too short and the seekable range is an exact time (can 32723 // happen in live with a 3 segment playlist), then don't use a time delta 32724 seekableStart === seekable.end(0) ? 0 : SAFE_TIME_DELTA); 32725 } 32726 32727 if (typeof seekTo !== 'undefined') { 32728 this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` + `seekable range ${printableRange(seekable)}. Seeking to ` + `${seekTo}.`); 32729 this.tech_.setCurrentTime(seekTo); 32730 return true; 32731 } 32732 32733 const sourceUpdater = this.playlistController_.sourceUpdater_; 32734 const buffered = this.tech_.buffered(); 32735 const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null; 32736 const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null; 32737 const media = this.media(); // verify that at least two segment durations or one part duration have been 32738 // appended before checking for a gap. 32739 32740 const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration : (media.targetDuration - TIME_FUDGE_FACTOR) * 2; // verify that at least two segment durations have been 32741 // appended before checking for a gap. 32742 32743 const bufferedToCheck = [audioBuffered, videoBuffered]; 32744 32745 for (let i = 0; i < bufferedToCheck.length; i++) { 32746 // skip null buffered 32747 if (!bufferedToCheck[i]) { 32748 continue; 32749 } 32750 32751 const timeAhead = timeAheadOf(bufferedToCheck[i], currentTime); // if we are less than two video/audio segment durations or one part 32752 // duration behind we haven't appended enough to call this a bad seek. 32753 32754 if (timeAhead < minAppendedDuration) { 32755 return false; 32756 } 32757 } 32758 32759 const nextRange = findNextRange(buffered, currentTime); // we have appended enough content, but we don't have anything buffered 32760 // to seek over the gap 32761 32762 if (nextRange.length === 0) { 32763 return false; 32764 } 32765 32766 seekTo = nextRange.start(0) + SAFE_TIME_DELTA; 32767 this.logger_(`Buffered region starts (${nextRange.start(0)}) ` + ` just beyond seek point (${currentTime}). Seeking to ${seekTo}.`); 32768 this.tech_.setCurrentTime(seekTo); 32769 return true; 32770 } 32771 /** 32772 * Handler for situations when we determine the player is waiting. 32773 * 32774 * @private 32775 */ 32776 32777 32778 waiting_() { 32779 if (this.techWaiting_()) { 32780 return; 32781 } // All tech waiting checks failed. Use last resort correction 32782 32783 32784 const currentTime = this.tech_.currentTime(); 32785 const buffered = this.tech_.buffered(); 32786 const currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered 32787 // region with no indication that anything is amiss (seen in Firefox). Seeking to 32788 // currentTime is usually enough to kickstart the player. This checks that the player 32789 // is currently within a buffered region before attempting a corrective seek. 32790 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 32791 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 32792 // make sure there is ~3 seconds of forward buffer before taking any corrective action 32793 // to avoid triggering an `unknownwaiting` event when the network is slow. 32794 32795 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 32796 this.resetTimeUpdate_(); 32797 this.tech_.setCurrentTime(currentTime); 32798 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 32799 32800 this.tech_.trigger({ 32801 type: 'usage', 32802 name: 'vhs-unknown-waiting' 32803 }); 32804 return; 32805 } 32806 } 32807 /** 32808 * Handler for situations when the tech fires a `waiting` event 32809 * 32810 * @return {boolean} 32811 * True if an action (or none) was needed to correct the waiting. False if no 32812 * checks passed 32813 * @private 32814 */ 32815 32816 32817 techWaiting_() { 32818 const seekable = this.seekable(); 32819 const currentTime = this.tech_.currentTime(); 32820 32821 if (this.tech_.seeking()) { 32822 // Tech is seeking or already waiting on another action, no action needed 32823 return true; 32824 } 32825 32826 if (this.beforeSeekableWindow_(seekable, currentTime)) { 32827 const livePoint = seekable.end(seekable.length - 1); 32828 this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` + `live point (seekable end) ${livePoint}`); 32829 this.resetTimeUpdate_(); 32830 this.tech_.setCurrentTime(livePoint); // live window resyncs may be useful for monitoring QoS 32831 32832 this.tech_.trigger({ 32833 type: 'usage', 32834 name: 'vhs-live-resync' 32835 }); 32836 return true; 32837 } 32838 32839 const sourceUpdater = this.tech_.vhs.playlistController_.sourceUpdater_; 32840 const buffered = this.tech_.buffered(); 32841 const videoUnderflow = this.videoUnderflow_({ 32842 audioBuffered: sourceUpdater.audioBuffered(), 32843 videoBuffered: sourceUpdater.videoBuffered(), 32844 currentTime 32845 }); 32846 32847 if (videoUnderflow) { 32848 // Even though the video underflowed and was stuck in a gap, the audio overplayed 32849 // the gap, leading currentTime into a buffered range. Seeking to currentTime 32850 // allows the video to catch up to the audio position without losing any audio 32851 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 32852 this.resetTimeUpdate_(); 32853 this.tech_.setCurrentTime(currentTime); // video underflow may be useful for monitoring QoS 32854 32855 this.tech_.trigger({ 32856 type: 'usage', 32857 name: 'vhs-video-underflow' 32858 }); 32859 return true; 32860 } 32861 32862 const nextRange = findNextRange(buffered, currentTime); // check for gap 32863 32864 if (nextRange.length > 0) { 32865 this.logger_(`Stopped at ${currentTime} and seeking to ${nextRange.start(0)}`); 32866 this.resetTimeUpdate_(); 32867 this.skipTheGap_(currentTime); 32868 return true; 32869 } // All checks failed. Returning false to indicate failure to correct waiting 32870 32871 32872 return false; 32873 } 32874 32875 afterSeekableWindow_(seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) { 32876 if (!seekable.length) { 32877 // we can't make a solid case if there's no seekable, default to false 32878 return false; 32879 } 32880 32881 let allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA; 32882 const isLive = !playlist.endList; 32883 const isLLHLS = typeof playlist.partTargetDuration === 'number'; 32884 32885 if (isLive && (isLLHLS || allowSeeksWithinUnsafeLiveWindow)) { 32886 allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3; 32887 } 32888 32889 if (currentTime > allowedEnd) { 32890 return true; 32891 } 32892 32893 return false; 32894 } 32895 32896 beforeSeekableWindow_(seekable, currentTime) { 32897 if (seekable.length && // can't fall before 0 and 0 seekable start identifies VOD stream 32898 seekable.start(0) > 0 && currentTime < seekable.start(0) - this.liveRangeSafeTimeDelta) { 32899 return true; 32900 } 32901 32902 return false; 32903 } 32904 32905 videoUnderflow_({ 32906 videoBuffered, 32907 audioBuffered, 32908 currentTime 32909 }) { 32910 // audio only content will not have video underflow :) 32911 if (!videoBuffered) { 32912 return; 32913 } 32914 32915 let gap; // find a gap in demuxed content. 32916 32917 if (videoBuffered.length && audioBuffered.length) { 32918 // in Chrome audio will continue to play for ~3s when we run out of video 32919 // so we have to check that the video buffer did have some buffer in the 32920 // past. 32921 const lastVideoRange = findRange(videoBuffered, currentTime - 3); 32922 const videoRange = findRange(videoBuffered, currentTime); 32923 const audioRange = findRange(audioBuffered, currentTime); 32924 32925 if (audioRange.length && !videoRange.length && lastVideoRange.length) { 32926 gap = { 32927 start: lastVideoRange.end(0), 32928 end: audioRange.end(0) 32929 }; 32930 } // find a gap in muxed content. 32931 32932 } else { 32933 const nextRange = findNextRange(videoBuffered, currentTime); // Even if there is no available next range, there is still a possibility we are 32934 // stuck in a gap due to video underflow. 32935 32936 if (!nextRange.length) { 32937 gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime); 32938 } 32939 } 32940 32941 if (gap) { 32942 this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` + `Seeking to current time ${currentTime}`); 32943 return true; 32944 } 32945 32946 return false; 32947 } 32948 /** 32949 * Timer callback. If playback still has not proceeded, then we seek 32950 * to the start of the next buffered region. 32951 * 32952 * @private 32953 */ 32954 32955 32956 skipTheGap_(scheduledCurrentTime) { 32957 const buffered = this.tech_.buffered(); 32958 const currentTime = this.tech_.currentTime(); 32959 const nextRange = findNextRange(buffered, currentTime); 32960 this.resetTimeUpdate_(); 32961 32962 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 32963 return; 32964 } 32965 32966 this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played 32967 32968 this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR); 32969
vendor: 3,428 bytes, lines 32969-33077
32969this.tech_.trigger({ 32970 type: 'usage', 32971 name: 'vhs-gap-skip' 32972 }); 32973 } 32974 32975 gapFromVideoUnderflow_(buffered, currentTime) { 32976 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 32977 // playing for ~3 seconds after the video gap starts. This is done to account for 32978 // video buffer underflow/underrun (note that this is not done when there is audio 32979 // buffer underflow/underrun -- in that case the video will stop as soon as it 32980 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 32981 // video stalls). The player's time will reflect the playthrough of audio, so the 32982 // time will appear as if we are in a buffered region, even if we are stuck in a 32983 // "gap." 32984 // 32985 // Example: 32986 // video buffer: 0 => 10.1, 10.2 => 20 32987 // audio buffer: 0 => 20 32988 // overall buffer: 0 => 10.1, 10.2 => 20 32989 // current time: 13 32990 // 32991 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 32992 // however, the audio continued playing until it reached ~3 seconds past the gap 32993 // (13 seconds), at which point it stops as well. Since current time is past the 32994 // gap, findNextRange will return no ranges. 32995 // 32996 // To check for this issue, we see if there is a gap that starts somewhere within 32997 // a 3 second range (3 seconds +/- 1 second) back from our current time. 32998 const gaps = findGaps(buffered); 32999 33000 for (let i = 0; i < gaps.length; i++) { 33001 const start = gaps.start(i); 33002 const end = gaps.end(i); // gap is starts no more than 4 seconds back 33003 33004 if (currentTime - start < 4 && currentTime - start > 2) { 33005 return { 33006 start, 33007 end 33008 }; 33009 } 33010 } 33011 33012 return null; 33013 } 33014 33015 } 33016 33017 const defaultOptions = { 33018 errorInterval: 30, 33019 33020 getSource(next) { 33021 const tech = this.tech({ 33022 IWillNotUseThisInPlugins: true 33023 }); 33024 const sourceObj = tech.currentSource_ || this.currentSource(); 33025 return next(sourceObj); 33026 } 33027 33028 }; 33029 /** 33030 * Main entry point for the plugin 33031 * 33032 * @param {Player} player a reference to a videojs Player instance 33033 * @param {Object} [options] an object with plugin options 33034 * @private 33035 */ 33036 33037 const initPlugin = function (player, options) { 33038 let lastCalled = 0; 33039 let seekTo = 0; 33040 const localOptions = merge$1(defaultOptions, options); 33041 player.ready(() => { 33042 player.trigger({ 33043 type: 'usage', 33044 name: 'vhs-error-reload-initialized' 33045 }); 33046 }); 33047 /** 33048 * Player modifications to perform that must wait until `loadedmetadata` 33049 * has been triggered 33050 * 33051 * @private 33052 */ 33053 33054 const loadedMetadataHandler = function () { 33055 if (seekTo) { 33056 player.currentTime(seekTo); 33057 } 33058 }; 33059 /** 33060 * Set the source on the player element, play, and seek if necessary 33061 * 33062 * @param {Object} sourceObj An object specifying the source url and mime-type to play 33063 * @private 33064 */ 33065 33066 33067 const setSource = function (sourceObj) { 33068 if (sourceObj === null || sourceObj === undefined) { 33069 return; 33070 } 33071 33072 seekTo = player.duration() !== Infinity && player.currentTime() || 0; 33073 player.one('loadedmetadata', loadedMetadataHandler); 33074 player.src(sourceObj); 33075 player.trigger({ 33076 type: 'usage', 33077 name: 'vhs-error-reload'
vendor: 2,342 bytes, lines 33078-33166
33078 }); 33079 player.play(); 33080 }; 33081 /** 33082 * Attempt to get a source from either the built-in getSource function 33083 * or a custom function provided via the options 33084 * 33085 * @private 33086 */ 33087 33088 33089 const errorHandler = function () { 33090 // Do not attempt to reload the source if a source-reload occurred before 33091 // 'errorInterval' time has elapsed since the last source-reload 33092 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 33093 player.trigger({ 33094 type: 'usage', 33095 name: 'vhs-error-reload-canceled' 33096 }); 33097 return; 33098 } 33099 33100 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 33101 videojs__default["default"].log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 33102 return; 33103 } 33104 33105 lastCalled = Date.now(); 33106 return localOptions.getSource.call(player, setSource); 33107 }; 33108 /** 33109 * Unbind any event handlers that were bound by the plugin 33110 * 33111 * @private 33112 */ 33113 33114 33115 const cleanupEvents = function () { 33116 player.off('loadedmetadata', loadedMetadataHandler); 33117 player.off('error', errorHandler); 33118 player.off('dispose', cleanupEvents); 33119 }; 33120 /** 33121 * Cleanup before re-initializing the plugin 33122 * 33123 * @param {Object} [newOptions] an object with plugin options 33124 * @private 33125 */ 33126 33127 33128 const reinitPlugin = function (newOptions) { 33129 cleanupEvents(); 33130 initPlugin(player, newOptions); 33131 }; 33132 33133 player.on('error', errorHandler); 33134 player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before 33135 // initializing the plugin 33136 33137 player.reloadSourceOnError = reinitPlugin; 33138 }; 33139 /** 33140 * Reload the source when an error is detected as long as there 33141 * wasn't an error previously within the last 30 seconds 33142 * 33143 * @param {Object} [options] an object with plugin options 33144 */ 33145 33146 33147 const reloadSourceOnError = function (options) { 33148 initPlugin(this, options); 33149 }; 33150 33151 var version$4 = "3.10.0"; 33152 33153 var version$3 = "7.0.2"; 33154 33155 var version$2 = "1.3.0"; 33156 33157 var version$1 = "7.1.0"; 33158 33159 var version = "4.0.1"; 33160 33161 /** 33162 * @file videojs-http-streaming.js 33163 * 33164 * The main file for the VHS project. 33165 * License: https://github.com/videojs/videojs-http-streaming/blob/main/LICENSE 33166 */
vendor: 20,703 bytes, lines 33166-33787
33166 33167 const Vhs = { 33168 PlaylistLoader, 33169 Playlist, 33170 utils, 33171 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 33172 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 33173 lastBandwidthSelector, 33174 movingAverageBandwidthSelector, 33175 comparePlaylistBandwidth, 33176 comparePlaylistResolution, 33177 xhr: xhrFactory() 33178 }; // Define getter/setters for config properties 33179 33180 Object.keys(Config).forEach(prop => { 33181 Object.defineProperty(Vhs, prop, { 33182 get() { 33183 videojs__default["default"].log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 33184 return Config[prop]; 33185 }, 33186 33187 set(value) { 33188 videojs__default["default"].log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 33189 33190 if (typeof value !== 'number' || value < 0) { 33191 videojs__default["default"].log.warn(`value of Vhs.${prop} must be greater than or equal to 0`); 33192 return; 33193 } 33194 33195 Config[prop] = value; 33196 } 33197 33198 }); 33199 }); 33200 const LOCAL_STORAGE_KEY = 'videojs-vhs'; 33201 /** 33202 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs. 33203 * 33204 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 33205 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 33206 * @function handleVhsMediaChange 33207 */ 33208 33209 const handleVhsMediaChange = function (qualityLevels, playlistLoader) { 33210 const newPlaylist = playlistLoader.media(); 33211 let selectedIndex = -1; 33212 33213 for (let i = 0; i < qualityLevels.length; i++) { 33214 if (qualityLevels[i].id === newPlaylist.id) { 33215 selectedIndex = i; 33216 break; 33217 } 33218 } 33219 33220 qualityLevels.selectedIndex_ = selectedIndex; 33221 qualityLevels.trigger({ 33222 selectedIndex, 33223 type: 'change' 33224 }); 33225 }; 33226 /** 33227 * Adds quality levels to list once playlist metadata is available 33228 * 33229 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 33230 * @param {Object} vhs Vhs object to listen to for media events. 33231 * @function handleVhsLoadedMetadata 33232 */ 33233 33234 33235 const handleVhsLoadedMetadata = function (qualityLevels, vhs) { 33236 vhs.representations().forEach(rep => { 33237 qualityLevels.addQualityLevel(rep); 33238 }); 33239 handleVhsMediaChange(qualityLevels, vhs.playlists); 33240 }; // VHS is a source handler, not a tech. Make sure attempts to use it 33241 // as one do not cause exceptions. 33242 33243 33244 Vhs.canPlaySource = function () { 33245 return videojs__default["default"].log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 33246 }; 33247 33248 const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => { 33249 if (!keySystemOptions) { 33250 return keySystemOptions; 33251 } 33252 33253 let codecs = {}; 33254 33255 if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) { 33256 codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS)); 33257 } 33258 33259 if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) { 33260 codecs.audio = audioPlaylist.attributes.CODECS; 33261 } 33262 33263 const videoContentType = getMimeForCodec(codecs.video); 33264 const audioContentType = getMimeForCodec(codecs.audio); // upsert the content types based on the selected playlist 33265 33266 const keySystemContentTypes = {}; 33267 33268 for (const keySystem in keySystemOptions) { 33269 keySystemContentTypes[keySystem] = {}; 33270 33271 if (audioContentType) { 33272 keySystemContentTypes[keySystem].audioContentType = audioContentType; 33273 } 33274 33275 if (videoContentType) { 33276 keySystemContentTypes[keySystem].videoContentType = videoContentType; 33277 } // Default to using the video playlist's PSSH even though they may be different, as 33278 // videojs-contrib-eme will only accept one in the options. 33279 // 33280 // This shouldn't be an issue for most cases as early intialization will handle all 33281 // unique PSSH values, and if they aren't, then encrypted events should have the 33282 // specific information needed for the unique license. 33283 33284 33285 if (mainPlaylist.contentProtection && mainPlaylist.contentProtection[keySystem] && mainPlaylist.contentProtection[keySystem].pssh) { 33286 keySystemContentTypes[keySystem].pssh = mainPlaylist.contentProtection[keySystem].pssh; 33287 } // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url' 33288 // so we need to prevent overwriting the URL entirely 33289 33290 33291 if (typeof keySystemOptions[keySystem] === 'string') { 33292 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 33293 } 33294 } 33295 33296 return merge$1(keySystemOptions, keySystemContentTypes); 33297 }; 33298 /** 33299 * @typedef {Object} KeySystems 33300 * 33301 * keySystems configuration for https://github.com/videojs/videojs-contrib-eme 33302 * Note: not all options are listed here. 33303 * 33304 * @property {Uint8Array} [pssh] 33305 * Protection System Specific Header 33306 */ 33307 33308 /** 33309 * Goes through all the playlists and collects an array of KeySystems options objects 33310 * containing each playlist's keySystems and their pssh values, if available. 33311 * 33312 * @param {Object[]} playlists 33313 * The playlists to look through 33314 * @param {string[]} keySystems 33315 * The keySystems to collect pssh values for 33316 * 33317 * @return {KeySystems[]} 33318 * An array of KeySystems objects containing available key systems and their 33319 * pssh values 33320 */ 33321 33322 33323 const getAllPsshKeySystemsOptions = (playlists, keySystems) => { 33324 return playlists.reduce((keySystemsArr, playlist) => { 33325 if (!playlist.contentProtection) { 33326 return keySystemsArr; 33327 } 33328 33329 const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => { 33330 const keySystemOptions = playlist.contentProtection[keySystem]; 33331 33332 if (keySystemOptions && keySystemOptions.pssh) { 33333 keySystemsObj[keySystem] = { 33334 pssh: keySystemOptions.pssh 33335 }; 33336 } 33337 33338 return keySystemsObj; 33339 }, {}); 33340 33341 if (Object.keys(keySystemsOptions).length) { 33342 keySystemsArr.push(keySystemsOptions); 33343 } 33344 33345 return keySystemsArr; 33346 }, []); 33347 }; 33348 /** 33349 * Returns a promise that waits for the 33350 * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session. 33351 * 33352 * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11 33353 * browsers. 33354 * 33355 * As per the above ticket, this is particularly important for Chrome, where, if 33356 * unencrypted content is appended before encrypted content and the key session has not 33357 * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached 33358 * during playback. 33359 * 33360 * @param {Object} player 33361 * The player instance 33362 * @param {Object[]} sourceKeySystems 33363 * The key systems options from the player source 33364 * @param {Object} [audioMedia] 33365 * The active audio media playlist (optional) 33366 * @param {Object[]} mainPlaylists 33367 * The playlists found on the main playlist object 33368 * 33369 * @return {Object} 33370 * Promise that resolves when the key session has been created 33371 */ 33372 33373 33374 const waitForKeySessionCreation = ({ 33375 player, 33376 sourceKeySystems, 33377 audioMedia, 33378 mainPlaylists 33379 }) => { 33380 if (!player.eme.initializeMediaKeys) { 33381 return Promise.resolve(); 33382 } // TODO should all audio PSSH values be initialized for DRM? 33383 // 33384 // All unique video rendition pssh values are initialized for DRM, but here only 33385 // the initial audio playlist license is initialized. In theory, an encrypted 33386 // event should be fired if the user switches to an alternative audio playlist 33387 // where a license is required, but this case hasn't yet been tested. In addition, there 33388 // may be many alternate audio playlists unlikely to be used (e.g., multiple different 33389 // languages). 33390 33391 33392 const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists; 33393 const keySystemsOptionsArr = getAllPsshKeySystemsOptions(playlists, Object.keys(sourceKeySystems)); 33394 const initializationFinishedPromises = []; 33395 const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The 33396 // only place where it should not be deduped is for ms-prefixed APIs, but 33397 // the existence of modern EME APIs in addition to 33398 // ms-prefixed APIs on Edge should prevent this from being a concern. 33399 // initializeMediaKeys also won't use the webkit-prefixed APIs. 33400 33401 keySystemsOptionsArr.forEach(keySystemsOptions => { 33402 keySessionCreatedPromises.push(new Promise((resolve, reject) => { 33403 player.tech_.one('keysessioncreated', resolve); 33404 })); 33405 initializationFinishedPromises.push(new Promise((resolve, reject) => { 33406 player.eme.initializeMediaKeys({ 33407 keySystems: keySystemsOptions 33408 }, err => { 33409 if (err) { 33410 reject(err); 33411 return; 33412 } 33413 33414 resolve(); 33415 }); 33416 })); 33417 }); // The reasons Promise.race is chosen over Promise.any: 33418 // 33419 // * Promise.any is only available in Safari 14+. 33420 // * None of these promises are expected to reject. If they do reject, it might be 33421 // better here for the race to surface the rejection, rather than mask it by using 33422 // Promise.any. 33423 33424 return Promise.race([// If a session was previously created, these will all finish resolving without 33425 // creating a new session, otherwise it will take until the end of all license 33426 // requests, which is why the key session check is used (to make setup much faster). 33427 Promise.all(initializationFinishedPromises), // Once a single session is created, the browser knows DRM will be used. 33428 Promise.race(keySessionCreatedPromises)]); 33429 }; 33430 /** 33431 * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and 33432 * there are keySystems on the source, sets up source options to prepare the source for 33433 * eme. 33434 * 33435 * @param {Object} player 33436 * The player instance 33437 * @param {Object[]} sourceKeySystems 33438 * The key systems options from the player source 33439 * @param {Object} media 33440 * The active media playlist 33441 * @param {Object} [audioMedia] 33442 * The active audio media playlist (optional) 33443 * 33444 * @return {boolean} 33445 * Whether or not options were configured and EME is available 33446 */ 33447 33448 const setupEmeOptions = ({ 33449 player, 33450 sourceKeySystems, 33451 media, 33452 audioMedia 33453 }) => { 33454 const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia); 33455 33456 if (!sourceOptions) { 33457 return false; 33458 } 33459 33460 player.currentSource().keySystems = sourceOptions; // eme handles the rest of the setup, so if it is missing 33461 // do nothing. 33462 33463 if (sourceOptions && !player.eme) { 33464 videojs__default["default"].log.warn('DRM encrypted source cannot be decrypted without a DRM plugin'); 33465 return false; 33466 } 33467 33468 return true; 33469 }; 33470 33471 const getVhsLocalStorage = () => { 33472 if (!window.localStorage) { 33473 return null; 33474 } 33475 33476 const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY); 33477 33478 if (!storedObject) { 33479 return null; 33480 } 33481 33482 try { 33483 return JSON.parse(storedObject); 33484 } catch (e) { 33485 // someone may have tampered with the value 33486 return null; 33487 } 33488 }; 33489 33490 const updateVhsLocalStorage = options => { 33491 if (!window.localStorage) { 33492 return false; 33493 } 33494 33495 let objectToStore = getVhsLocalStorage(); 33496 objectToStore = objectToStore ? merge$1(objectToStore, options) : options; 33497 33498 try { 33499 window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore)); 33500 } catch (e) { 33501 // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where 33502 // storage is set to 0). 33503 // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions 33504 // No need to perform any operation. 33505 return false; 33506 } 33507 33508 return objectToStore; 33509 }; 33510 /** 33511 * Parses VHS-supported media types from data URIs. See 33512 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 33513 * for information on data URIs. 33514 * 33515 * @param {string} dataUri 33516 * The data URI 33517 * 33518 * @return {string|Object} 33519 * The parsed object/string, or the original string if no supported media type 33520 * was found 33521 */ 33522 33523 33524 const expandDataUri = dataUri => { 33525 if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) { 33526 return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1)); 33527 } // no known case for this data URI, return the string as-is 33528 33529 33530 return dataUri; 33531 }; 33532 /** 33533 * Adds a request hook to an xhr object 33534 * 33535 * @param {Object} xhr object to add the onRequest hook to 33536 * @param {function} callback hook function for an xhr request 33537 */ 33538 33539 33540 const addOnRequestHook = (xhr, callback) => { 33541 if (!xhr._requestCallbackSet) { 33542 xhr._requestCallbackSet = new Set(); 33543 } 33544 33545 xhr._requestCallbackSet.add(callback); 33546 }; 33547 /** 33548 * Adds a response hook to an xhr object 33549 * 33550 * @param {Object} xhr object to add the onResponse hook to 33551 * @param {function} callback hook function for an xhr response 33552 */ 33553 33554 33555 const addOnResponseHook = (xhr, callback) => { 33556 if (!xhr._responseCallbackSet) { 33557 xhr._responseCallbackSet = new Set(); 33558 } 33559 33560 xhr._responseCallbackSet.add(callback); 33561 }; 33562 /** 33563 * Removes a request hook on an xhr object, deletes the onRequest set if empty. 33564 * 33565 * @param {Object} xhr object to remove the onRequest hook from 33566 * @param {function} callback hook function to remove 33567 */ 33568 33569 33570 const removeOnRequestHook = (xhr, callback) => { 33571 if (!xhr._requestCallbackSet) { 33572 return; 33573 } 33574 33575 xhr._requestCallbackSet.delete(callback); 33576 33577 if (!xhr._requestCallbackSet.size) { 33578 delete xhr._requestCallbackSet; 33579 } 33580 }; 33581 /** 33582 * Removes a response hook on an xhr object, deletes the onResponse set if empty. 33583 * 33584 * @param {Object} xhr object to remove the onResponse hook from 33585 * @param {function} callback hook function to remove 33586 */ 33587 33588 33589 const removeOnResponseHook = (xhr, callback) => { 33590 if (!xhr._responseCallbackSet) { 33591 return; 33592 } 33593 33594 xhr._responseCallbackSet.delete(callback); 33595 33596 if (!xhr._responseCallbackSet.size) { 33597 delete xhr._responseCallbackSet; 33598 } 33599 }; 33600 /** 33601 * Whether the browser has built-in HLS support. 33602 */ 33603 33604 33605 Vhs.supportsNativeHls = function () { 33606 if (!document || !document.createElement) { 33607 return false; 33608 } 33609 33610 const video = document.createElement('video'); // native HLS is definitely not supported if HTML5 video isn't 33611 33612 if (!videojs__default["default"].getTech('Html5').isSupported()) { 33613 return false; 33614 } // HLS manifests can go by many mime-types 33615 33616 33617 const canPlay = [// Apple santioned 33618 'application/vnd.apple.mpegurl', // Apple sanctioned for backwards compatibility 33619 'audio/mpegurl', // Very common 33620 'audio/x-mpegurl', // Very common 33621 'application/x-mpegurl', // Included for completeness 33622 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; 33623 return canPlay.some(function (canItPlay) { 33624 return /maybe|probably/i.test(video.canPlayType(canItPlay)); 33625 }); 33626 }(); 33627 33628 Vhs.supportsNativeDash = function () { 33629 if (!document || !document.createElement || !videojs__default["default"].getTech('Html5').isSupported()) { 33630 return false; 33631 } 33632 33633 return /maybe|probably/i.test(document.createElement('video').canPlayType('application/dash+xml')); 33634 }(); 33635 33636 Vhs.supportsTypeNatively = type => { 33637 if (type === 'hls') { 33638 return Vhs.supportsNativeHls; 33639 } 33640 33641 if (type === 'dash') { 33642 return Vhs.supportsNativeDash; 33643 } 33644 33645 return false; 33646 }; 33647 /** 33648 * VHS is a source handler, not a tech. Make sure attempts to use it 33649 * as one do not cause exceptions. 33650 */ 33651 33652 33653 Vhs.isSupported = function () { 33654 return videojs__default["default"].log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 33655 }; 33656 /** 33657 * A global function for setting an onRequest hook 33658 * 33659 * @param {function} callback for request modifiction 33660 */ 33661 33662 33663 Vhs.xhr.onRequest = function (callback) { 33664 addOnRequestHook(Vhs.xhr, callback); 33665 }; 33666 /** 33667 * A global function for setting an onResponse hook 33668 * 33669 * @param {callback} callback for response data retrieval 33670 */ 33671 33672 33673 Vhs.xhr.onResponse = function (callback) { 33674 addOnResponseHook(Vhs.xhr, callback); 33675 }; 33676 /** 33677 * Deletes a global onRequest callback if it exists 33678 * 33679 * @param {function} callback to delete from the global set 33680 */ 33681 33682 33683 Vhs.xhr.offRequest = function (callback) { 33684 removeOnRequestHook(Vhs.xhr, callback); 33685 }; 33686 /** 33687 * Deletes a global onResponse callback if it exists 33688 * 33689 * @param {function} callback to delete from the global set 33690 */ 33691 33692 33693 Vhs.xhr.offResponse = function (callback) { 33694 removeOnResponseHook(Vhs.xhr, callback); 33695 }; 33696 33697 const Component = videojs__default["default"].getComponent('Component'); 33698 /** 33699 * The Vhs Handler object, where we orchestrate all of the parts 33700 * of VHS to interact with video.js 33701 * 33702 * @class VhsHandler 33703 * @extends videojs.Component 33704 * @param {Object} source the soruce object 33705 * @param {Tech} tech the parent tech object 33706 * @param {Object} options optional and required options 33707 */ 33708 33709 class VhsHandler extends Component { 33710 constructor(source, tech, options) { 33711 super(tech, options.vhs); // if a tech level `initialBandwidth` option was passed 33712 // use that over the VHS level `bandwidth` option 33713 33714 if (typeof options.initialBandwidth === 'number') { 33715 this.options_.bandwidth = options.initialBandwidth; 33716 } 33717 33718 this.logger_ = logger('VhsHandler'); // we need access to the player in some cases, 33719 // so, get it from Video.js via the `playerId` 33720 33721 if (tech.options_ && tech.options_.playerId) { 33722 const _player = videojs__default["default"].getPlayer(tech.options_.playerId); 33723 33724 this.player_ = _player; 33725 } 33726 33727 this.tech_ = tech; 33728 this.source_ = source; 33729 this.stats = {}; 33730 this.ignoreNextSeekingEvent_ = false; 33731 this.setOptions_(); 33732 33733 if (this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) { 33734 tech.overrideNativeAudioTracks(true); 33735 tech.overrideNativeVideoTracks(true); 33736 } else if (this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { 33737 // overriding native VHS only works if audio tracks have been emulated 33738 // error early if we're misconfigured 33739 throw new Error('Overriding native VHS requires emulated tracks. ' + 'See https://git.io/vMpjB'); 33740 } // listen for fullscreenchange events for this player so that we 33741 // can adjust our quality selection quickly 33742 33743 33744 this.on(document, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], event => { 33745 const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; 33746 33747 if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) { 33748 this.playlistController_.fastQualityChange_(); 33749 } else { 33750 // When leaving fullscreen, since the in page pixel dimensions should be smaller 33751 // than full screen, see if there should be a rendition switch down to preserve 33752 // bandwidth. 33753 this.playlistController_.checkABR_(); 33754 } 33755 }); 33756 this.on(this.tech_, 'seeking', function () { 33757 if (this.ignoreNextSeekingEvent_) { 33758 this.ignoreNextSeekingEvent_ = false; 33759 return; 33760 } 33761 33762 this.setCurrentTime(this.tech_.currentTime()); 33763 }); 33764 this.on(this.tech_, 'error', function () { 33765 // verify that the error was real and we are loaded 33766 // enough to have pc loaded. 33767 if (this.tech_.error() && this.playlistController_) { 33768 this.playlistController_.pauseLoading(); 33769 } 33770 }); 33771 this.on(this.tech_, 'play', this.play); 33772 } 33773 /** 33774 * Set VHS options based on options from configuration, as well as partial 33775 * options to be passed at a later time. 33776 * 33777 * @param {Object} options A partial chunk of config options 33778 */ 33779 33780 33781 setOptions_(options = {}) { 33782 this.options_ = merge$1(this.options_, options); // defaults 33783 33784 this.options_.withCredentials = this.options_.withCredentials || false; 33785 this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true; 33786 this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false; 33787 this.options_.use
vendor: 4,208 bytes, lines 33787-33878
33787BandwidthFromLocalStorage = typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false; 33788 this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false; 33789 this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false; 33790 this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false; 33791 this.options_.customTagParsers = this.options_.customTagParsers || []; 33792 this.options_.customTagMappers = this.options_.customTagMappers || []; 33793 this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; 33794 this.options_.llhls = this.options_.llhls === false ? false : true; 33795 this.options_.bufferBasedABR = this.options_.bufferBasedABR || false; 33796 33797 if (typeof this.options_.playlistExclusionDuration !== 'number') { 33798 this.options_.playlistExclusionDuration = 60; 33799 } 33800 33801 if (typeof this.options_.bandwidth !== 'number') { 33802 if (this.options_.useBandwidthFromLocalStorage) { 33803 const storedObject = getVhsLocalStorage(); 33804 33805 if (storedObject && storedObject.bandwidth) { 33806 this.options_.bandwidth = storedObject.bandwidth; 33807 this.tech_.trigger({ 33808 type: 'usage', 33809 name: 'vhs-bandwidth-from-local-storage' 33810 }); 33811 } 33812 33813 if (storedObject && storedObject.throughput) { 33814 this.options_.throughput = storedObject.throughput; 33815 this.tech_.trigger({ 33816 type: 'usage', 33817 name: 'vhs-throughput-from-local-storage' 33818 }); 33819 } 33820 } 33821 } // if bandwidth was not set by options or pulled from local storage, start playlist 33822 // selection at a reasonable bandwidth 33823 33824 33825 if (typeof this.options_.bandwidth !== 'number') { 33826 this.options_.bandwidth = Config.INITIAL_BANDWIDTH; 33827 } // If the bandwidth number is unchanged from the initial setting 33828 // then this takes precedence over the enableLowInitialPlaylist option 33829 33830 33831 this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src 33832 33833 ['withCredentials', 'useDevicePixelRatio', 'limitRenditionByPlayerDimensions', 'bandwidth', 'customTagParsers', 'customTagMappers', 'cacheEncryptionKeys', 'playlistSelector', 'initialPlaylistSelector', 'bufferBasedABR', 'liveRangeSafeTimeDelta', 'llhls', 'useForcedSubtitles', 'useNetworkInformationApi', 'useDtsForTimestampOffset', 'exactManifestTimings', 'leastPixelDiffSelector'].forEach(option => { 33834 if (typeof this.source_[option] !== 'undefined') { 33835 this.options_[option] = this.source_[option]; 33836 } 33837 }); 33838 this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions; 33839 this.useDevicePixelRatio = this.options_.useDevicePixelRatio; 33840 } // alias for public method to set options 33841 33842 33843 setOptions(options = {}) { 33844 this.setOptions_(options); 33845 } 33846 /** 33847 * called when player.src gets called, handle a new source 33848 * 33849 * @param {Object} src the source object to handle 33850 */ 33851 33852 33853 src(src, type) { 33854 // do nothing if the src is falsey 33855 if (!src) { 33856 return; 33857 } 33858 33859 this.setOptions_(); // add main playlist controller options 33860 33861 this.options_.src = expandDataUri(this.source_.src); 33862 this.options_.tech = this.tech_; 33863 this.options_.externVhs = Vhs; 33864 this.options_.sourceType = simpleTypeFromSourceType(type); // Whenever we seek internally, we should update the tech 33865 33866 this.options_.seekTo = time => { 33867 this.tech_.setCurrentTime(time); 33868 }; 33869 33870 this.playlistController_ = new PlaylistController(this.options_); 33871 const playbackWatcherOptions = merge$1({ 33872 liveRangeSafeTimeDelta: SAFE_TIME_DELTA 33873 }, this.options_, { 33874 seekable: () => this.seekable(), 33875 media: () => this.playlistController_.media(), 33876 playlistController: this.playlistController_ 33877 }); 33878 this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions
vendor: 9,494 bytes, lines 33878-34131
33878); 33879 this.playlistController_.on('error', () => { 33880 const player = videojs__default["default"].players[this.tech_.options_.playerId]; 33881 let error = this.playlistController_.error; 33882 33883 if (typeof error === 'object' && !error.code) { 33884 error.code = 3; 33885 } else if (typeof error === 'string') { 33886 error = { 33887 message: error, 33888 code: 3 33889 }; 33890 } 33891 33892 player.error(error); 33893 }); 33894 const defaultSelector = this.options_.bufferBasedABR ? Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR; // `this` in selectPlaylist should be the VhsHandler for backwards 33895 // compatibility with < v2 33896 33897 this.playlistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : defaultSelector.bind(this); 33898 this.playlistController_.selectInitialPlaylist = Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 33899 33900 this.playlists = this.playlistController_.mainPlaylistLoader_; 33901 this.mediaSource = this.playlistController_.mediaSource; // Proxy assignment of some properties to the main playlist 33902 // controller. Using a custom property for backwards compatibility 33903 // with < v2 33904 33905 Object.defineProperties(this, { 33906 selectPlaylist: { 33907 get() { 33908 return this.playlistController_.selectPlaylist; 33909 }, 33910 33911 set(selectPlaylist) { 33912 this.playlistController_.selectPlaylist = selectPlaylist.bind(this); 33913 } 33914 33915 }, 33916 throughput: { 33917 get() { 33918 return this.playlistController_.mainSegmentLoader_.throughput.rate; 33919 }, 33920 33921 set(throughput) { 33922 this.playlistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value 33923 // for the cumulative average 33924 33925 this.playlistController_.mainSegmentLoader_.throughput.count = 1; 33926 } 33927 33928 }, 33929 bandwidth: { 33930 get() { 33931 let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth; 33932 const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection; 33933 const tenMbpsAsBitsPerSecond = 10e6; 33934 33935 if (this.options_.useNetworkInformationApi && networkInformation) { 33936 // downlink returns Mbps 33937 // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink 33938 const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player 33939 // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that 33940 // high quality streams are not filtered out. 33941 33942 if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) { 33943 playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec); 33944 } else { 33945 playerBandwidthEst = networkInfoBandwidthEstBitsPerSec; 33946 } 33947 } 33948 33949 return playerBandwidthEst; 33950 }, 33951 33952 set(bandwidth) { 33953 this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter 33954 // `count` is set to zero that current value of `rate` isn't included 33955 // in the cumulative average 33956 33957 this.playlistController_.mainSegmentLoader_.throughput = { 33958 rate: 0, 33959 count: 0 33960 }; 33961 } 33962 33963 }, 33964 33965 /** 33966 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 33967 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 33968 * the entire process after that - decryption, transmuxing, and appending - provided 33969 * by `throughput`. 33970 * 33971 * Since the two process are serial, the overall system bandwidth is given by: 33972 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 33973 */ 33974 systemBandwidth: { 33975 get() { 33976 const invBandwidth = 1 / (this.bandwidth || 1); 33977 let invThroughput; 33978 33979 if (this.throughput > 0) { 33980 invThroughput = 1 / this.throughput; 33981 } else { 33982 invThroughput = 0; 33983 } 33984 33985 const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 33986 return systemBitrate; 33987 }, 33988 33989 set() { 33990 videojs__default["default"].log.error('The "systemBandwidth" property is read-only'); 33991 } 33992 33993 } 33994 }); 33995 33996 if (this.options_.bandwidth) { 33997 this.bandwidth = this.options_.bandwidth; 33998 } 33999 34000 if (this.options_.throughput) { 34001 this.throughput = this.options_.throughput; 34002 } 34003 34004 Object.defineProperties(this.stats, { 34005 bandwidth: { 34006 get: () => this.bandwidth || 0, 34007 enumerable: true 34008 }, 34009 mediaRequests: { 34010 get: () => this.playlistController_.mediaRequests_() || 0, 34011 enumerable: true 34012 }, 34013 mediaRequestsAborted: { 34014 get: () => this.playlistController_.mediaRequestsAborted_() || 0, 34015 enumerable: true 34016 }, 34017 mediaRequestsTimedout: { 34018 get: () => this.playlistController_.mediaRequestsTimedout_() || 0, 34019 enumerable: true 34020 }, 34021 mediaRequestsErrored: { 34022 get: () => this.playlistController_.mediaRequestsErrored_() || 0, 34023 enumerable: true 34024 }, 34025 mediaTransferDuration: { 34026 get: () => this.playlistController_.mediaTransferDuration_() || 0, 34027 enumerable: true 34028 }, 34029 mediaBytesTransferred: { 34030 get: () => this.playlistController_.mediaBytesTransferred_() || 0, 34031 enumerable: true 34032 }, 34033 mediaSecondsLoaded: { 34034 get: () => this.playlistController_.mediaSecondsLoaded_() || 0, 34035 enumerable: true 34036 }, 34037 mediaAppends: { 34038 get: () => this.playlistController_.mediaAppends_() || 0, 34039 enumerable: true 34040 }, 34041 mainAppendsToLoadedData: { 34042 get: () => this.playlistController_.mainAppendsToLoadedData_() || 0, 34043 enumerable: true 34044 }, 34045 audioAppendsToLoadedData: { 34046 get: () => this.playlistController_.audioAppendsToLoadedData_() || 0, 34047 enumerable: true 34048 }, 34049 appendsToLoadedData: { 34050 get: () => this.playlistController_.appendsToLoadedData_() || 0, 34051 enumerable: true 34052 }, 34053 timeToLoadedData: { 34054 get: () => this.playlistController_.timeToLoadedData_() || 0, 34055 enumerable: true 34056 }, 34057 buffered: { 34058 get: () => timeRangesToArray(this.tech_.buffered()), 34059 enumerable: true 34060 }, 34061 currentTime: { 34062 get: () => this.tech_.currentTime(), 34063 enumerable: true 34064 }, 34065 currentSource: { 34066 get: () => this.tech_.currentSource_, 34067 enumerable: true 34068 }, 34069 currentTech: { 34070 get: () => this.tech_.name_, 34071 enumerable: true 34072 }, 34073 duration: { 34074 get: () => this.tech_.duration(), 34075 enumerable: true 34076 }, 34077 main: { 34078 get: () => this.playlists.main, 34079 enumerable: true 34080 }, 34081 playerDimensions: { 34082 get: () => this.tech_.currentDimensions(), 34083 enumerable: true 34084 }, 34085 seekable: { 34086 get: () => timeRangesToArray(this.tech_.seekable()), 34087 enumerable: true 34088 }, 34089 timestamp: { 34090 get: () => Date.now(), 34091 enumerable: true 34092 }, 34093 videoPlaybackQuality: { 34094 get: () => this.tech_.getVideoPlaybackQuality(), 34095 enumerable: true 34096 } 34097 }); 34098 this.tech_.one('canplay', this.playlistController_.setupFirstPlay.bind(this.playlistController_)); 34099 this.tech_.on('bandwidthupdate', () => { 34100 if (this.options_.useBandwidthFromLocalStorage) { 34101 updateVhsLocalStorage({ 34102 bandwidth: this.bandwidth, 34103 throughput: Math.round(this.throughput) 34104 }); 34105 } 34106 }); 34107 this.playlistController_.on('selectedinitialmedia', () => { 34108 // Add the manual rendition mix-in to VhsHandler 34109 renditionSelectionMixin(this); 34110 }); 34111 this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => { 34112 this.setupEme_(); 34113 }); // the bandwidth of the primary segment loader is our best 34114 // estimate of overall bandwidth 34115 34116 this.on(this.playlistController_, 'progress', function () { 34117 this.tech_.trigger('progress'); 34118 }); // In the live case, we need to ignore the very first `seeking` event since 34119 // that will be the result of the seek-to-live behavior 34120 34121 this.on(this.playlistController_, 'firstplay', function () { 34122 this.ignoreNextSeekingEvent_ = true; 34123 }); 34124 this.setupQualityLevels_(); // do nothing if the tech has been disposed already 34125 // this can occur if someone sets the src in player.ready(), for instance 34126 34127 if (!this.tech_.el()) { 34128 return; 34129 } 34130 34131 this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource);
34131 34132 this.tech_.src(this.mediaSourceUrl_);
vendor: 7,277 bytes, lines 34132-34386
34132 34133 } 34134 34135 createKeySessions_() { 34136 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 34137 this.logger_('waiting for EME key session creation'); 34138 waitForKeySessionCreation({ 34139 player: this.player_, 34140 sourceKeySystems: this.source_.keySystems, 34141 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(), 34142 mainPlaylists: this.playlists.main.playlists 34143 }).then(() => { 34144 this.logger_('created EME key session'); 34145 this.playlistController_.sourceUpdater_.initializedEme(); 34146 }).catch(err => { 34147 this.logger_('error while creating EME key session', err); 34148 this.player_.error({ 34149 message: 'Failed to initialize media keys for EME', 34150 code: 3 34151 }); 34152 }); 34153 } 34154 34155 handleWaitingForKey_() { 34156 // If waitingforkey is fired, it's possible that the data that's necessary to retrieve 34157 // the key is in the manifest. While this should've happened on initial source load, it 34158 // may happen again in live streams where the keys change, and the manifest info 34159 // reflects the update. 34160 // 34161 // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's 34162 // already requested keys for, we don't have to worry about this generating extraneous 34163 // requests. 34164 this.logger_('waitingforkey fired, attempting to create any new key sessions'); 34165 this.createKeySessions_(); 34166 } 34167 /** 34168 * If necessary and EME is available, sets up EME options and waits for key session 34169 * creation. 34170 * 34171 * This function also updates the source updater so taht it can be used, as for some 34172 * browsers, EME must be configured before content is appended (if appending unencrypted 34173 * content before encrypted content). 34174 */ 34175 34176 34177 setupEme_() { 34178 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 34179 const didSetupEmeOptions = setupEmeOptions({ 34180 player: this.player_, 34181 sourceKeySystems: this.source_.keySystems, 34182 media: this.playlists.media(), 34183 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media() 34184 }); 34185 this.player_.tech_.on('keystatuschange', e => { 34186 this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status); 34187 }); 34188 this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this); 34189 this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_); 34190 34191 if (!didSetupEmeOptions) { 34192 // If EME options were not set up, we've done all we could to initialize EME. 34193 this.playlistController_.sourceUpdater_.initializedEme(); 34194 return; 34195 } 34196 34197 this.createKeySessions_(); 34198 } 34199 /** 34200 * Initializes the quality levels and sets listeners to update them. 34201 * 34202 * @method setupQualityLevels_ 34203 * @private 34204 */ 34205 34206 34207 setupQualityLevels_() { 34208 const player = videojs__default["default"].players[this.tech_.options_.playerId]; // if there isn't a player or there isn't a qualityLevels plugin 34209 // or qualityLevels_ listeners have already been setup, do nothing. 34210 34211 if (!player || !player.qualityLevels || this.qualityLevels_) { 34212 return; 34213 } 34214 34215 this.qualityLevels_ = player.qualityLevels(); 34216 this.playlistController_.on('selectedinitialmedia', () => { 34217 handleVhsLoadedMetadata(this.qualityLevels_, this); 34218 }); 34219 this.playlists.on('mediachange', () => { 34220 handleVhsMediaChange(this.qualityLevels_, this.playlists); 34221 }); 34222 } 34223 /** 34224 * return the version 34225 */ 34226 34227 34228 static version() { 34229 return { 34230 '@videojs/http-streaming': version$4, 34231 'mux.js': version$3, 34232 'mpd-parser': version$2, 34233 'm3u8-parser': version$1, 34234 'aes-decrypter': version 34235 }; 34236 } 34237 /** 34238 * return the version 34239 */ 34240 34241 34242 version() { 34243 return this.constructor.version(); 34244 } 34245 34246 canChangeType() { 34247 return SourceUpdater.canChangeType(); 34248 } 34249 /** 34250 * Begin playing the video. 34251 */ 34252 34253 34254 play() { 34255 this.playlistController_.play(); 34256 } 34257 /** 34258 * a wrapper around the function in PlaylistController 34259 */ 34260 34261 34262 setCurrentTime(currentTime) { 34263 this.playlistController_.setCurrentTime(currentTime); 34264 } 34265 /** 34266 * a wrapper around the function in PlaylistController 34267 */ 34268 34269 34270 duration() { 34271 return this.playlistController_.duration(); 34272 } 34273 /** 34274 * a wrapper around the function in PlaylistController 34275 */ 34276 34277 34278 seekable() { 34279 return this.playlistController_.seekable(); 34280 } 34281 /** 34282 * Abort all outstanding work and cleanup. 34283 */ 34284 34285 34286 dispose() { 34287 if (this.playbackWatcher_) { 34288 this.playbackWatcher_.dispose(); 34289 } 34290 34291 if (this.playlistController_) { 34292 this.playlistController_.dispose(); 34293 } 34294 34295 if (this.qualityLevels_) { 34296 this.qualityLevels_.dispose(); 34297 } 34298 34299 if (this.tech_ && this.tech_.vhs) { 34300 delete this.tech_.vhs; 34301 } 34302 34303 if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) { 34304 window.URL.revokeObjectURL(this.mediaSourceUrl_); 34305 this.mediaSourceUrl_ = null; 34306 } 34307 34308 if (this.tech_) { 34309 this.tech_.off('waitingforkey', this.handleWaitingForKey_); 34310 } 34311 34312 super.dispose(); 34313 } 34314 34315 convertToProgramTime(time, callback) { 34316 return getProgramTime({ 34317 playlist: this.playlistController_.media(), 34318 time, 34319 callback 34320 }); 34321 } // the player must be playing before calling this 34322 34323 34324 seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) { 34325 return seekToProgramTime({ 34326 programTime, 34327 playlist: this.playlistController_.media(), 34328 retryCount, 34329 pauseAfterSeek, 34330 seekTo: this.options_.seekTo, 34331 tech: this.options_.tech, 34332 callback 34333 }); 34334 } 34335 /** 34336 * Adds the onRequest, onResponse, offRequest and offResponse functions 34337 * to the VhsHandler xhr Object. 34338 */ 34339 34340 34341 setupXhrHooks_() { 34342 /** 34343 * A player function for setting an onRequest hook 34344 * 34345 * @param {function} callback for request modifiction 34346 */ 34347 this.xhr.onRequest = callback => { 34348 addOnRequestHook(this.xhr, callback); 34349 }; 34350 /** 34351 * A player function for setting an onResponse hook 34352 * 34353 * @param {callback} callback for response data retrieval 34354 */ 34355 34356 34357 this.xhr.onResponse = callback => { 34358 addOnResponseHook(this.xhr, callback); 34359 }; 34360 /** 34361 * Deletes a player onRequest callback if it exists 34362 * 34363 * @param {function} callback to delete from the player set 34364 */ 34365 34366 34367 this.xhr.offRequest = callback => { 34368 removeOnRequestHook(this.xhr, callback); 34369 }; 34370 /** 34371 * Deletes a player onResponse callback if it exists 34372 * 34373 * @param {function} callback to delete from the player set 34374 */ 34375 34376 34377 this.xhr.offResponse = callback => { 34378 removeOnResponseHook(this.xhr, callback); 34379 }; // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks. 34380 // This allows hooks to be set before the source is set to vhs when handleSource is called. 34381 34382 34383 this.player_.trigger('xhr-hooks-ready'); 34384 } 34385 34386
vendor: 3,360 bytes, lines 34386-34482
34386} 34387 /** 34388 * The Source Handler object, which informs video.js what additional 34389 * MIME types are supported and sets up playback. It is registered 34390 * automatically to the appropriate tech based on the capabilities of 34391 * the browser it is running in. It is not necessary to use or modify 34392 * this object in normal usage. 34393 */ 34394 34395 34396 const VhsSourceHandler = { 34397 name: 'videojs-http-streaming', 34398 VERSION: version$4, 34399 34400 canHandleSource(srcObj, options = {}) { 34401 const localOptions = merge$1(videojs__default["default"].options, options); 34402 return VhsSourceHandler.canPlayType(srcObj.type, localOptions); 34403 }, 34404 34405 handleSource(source, tech, options = {}) { 34406 const localOptions = merge$1(videojs__default["default"].options, options); 34407 tech.vhs = new VhsHandler(source, tech, localOptions); 34408 tech.vhs.xhr = xhrFactory(); 34409 tech.vhs.setupXhrHooks_(); 34410 tech.vhs.src(source.src, source.type); 34411 return tech.vhs; 34412 }, 34413 34414 canPlayType(type, options) { 34415 const simpleType = simpleTypeFromSourceType(type); 34416 34417 if (!simpleType) { 34418 return ''; 34419 } 34420 34421 const overrideNative = VhsSourceHandler.getOverrideNative(options); 34422 const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType); 34423 const canUseMsePlayback = !supportsTypeNatively || overrideNative; 34424 return canUseMsePlayback ? 'maybe' : ''; 34425 }, 34426 34427 getOverrideNative(options = {}) { 34428 const { 34429 vhs = {} 34430 } = options; 34431 const defaultOverrideNative = !(videojs__default["default"].browser.IS_ANY_SAFARI || videojs__default["default"].browser.IS_IOS); 34432 const { 34433 overrideNative = defaultOverrideNative 34434 } = vhs; 34435 return overrideNative; 34436 } 34437 34438 }; 34439 /** 34440 * Check to see if the native MediaSource object exists and supports 34441 * an MP4 container with both H.264 video and AAC-LC audio. 34442 * 34443 * @return {boolean} if native media sources are supported 34444 */ 34445 34446 const supportsNativeMediaSources = () => { 34447 return browserSupportsCodec('avc1.4d400d,mp4a.40.2'); 34448 }; // register source handlers with the appropriate techs 34449 34450 34451 if (supportsNativeMediaSources()) { 34452 videojs__default["default"].getTech('Html5').registerSourceHandler(VhsSourceHandler, 0); 34453 } 34454 34455 videojs__default["default"].VhsHandler = VhsHandler; 34456 videojs__default["default"].VhsSourceHandler = VhsSourceHandler; 34457 videojs__default["default"].Vhs = Vhs; 34458 34459 if (!videojs__default["default"].use) { 34460 videojs__default["default"].registerComponent('Vhs', Vhs); 34461 } 34462 34463 videojs__default["default"].options.vhs = videojs__default["default"].options.vhs || {}; 34464 34465 if (!videojs__default["default"].getPlugin || !videojs__default["default"].getPlugin('reloadSourceOnError')) { 34466 videojs__default["default"].registerPlugin('reloadSourceOnError', reloadSourceOnError); 34467 } 34468 34469 exports.LOCAL_STORAGE_KEY = LOCAL_STORAGE_KEY; 34470 exports.Vhs = Vhs; 34471 exports.VhsHandler = VhsHandler; 34472 exports.VhsSourceHandler = VhsSourceHandler; 34473 exports.emeKeySystems = emeKeySystems; 34474 exports.expandDataUri = expandDataUri; 34475 exports.getAllPsshKeySystemsOptions = getAllPsshKeySystemsOptions; 34476 exports.setupEmeOptions = setupEmeOptions; 34477 exports.simpleTypeFromSourceType = simpleTypeFromSourceType; 34478 exports.waitForKeySessionCreation = waitForKeySessionCreation; 34479 34480 Object.defineProperty(exports, '__esModule', { value: true }); 34481 34482}));
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.