shaka-player 4.7.13 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/AUTHORS +1 -0
  2. package/CONTRIBUTORS +1 -0
  3. package/LICENSE +54 -0
  4. package/README.md +22 -1
  5. package/dist/controls.css +1 -1
  6. package/dist/controls.css.map +1 -1
  7. package/dist/deps.js +34 -23
  8. package/dist/locales.js +44 -34
  9. package/dist/shaka-player.compiled.d.ts +194 -44
  10. package/dist/shaka-player.compiled.debug.d.ts +194 -44
  11. package/dist/shaka-player.compiled.debug.externs.js +474 -337
  12. package/dist/shaka-player.compiled.debug.js +949 -832
  13. package/dist/shaka-player.compiled.debug.map +4 -4
  14. package/dist/shaka-player.compiled.externs.js +474 -337
  15. package/dist/shaka-player.compiled.js +1237 -1171
  16. package/dist/shaka-player.compiled.map +4 -4
  17. package/dist/shaka-player.ui.d.ts +257 -47
  18. package/dist/shaka-player.ui.debug.d.ts +257 -47
  19. package/dist/shaka-player.ui.debug.externs.js +591 -376
  20. package/dist/shaka-player.ui.debug.js +1165 -965
  21. package/dist/shaka-player.ui.debug.map +4 -4
  22. package/dist/shaka-player.ui.externs.js +591 -376
  23. package/dist/shaka-player.ui.js +1521 -1404
  24. package/dist/shaka-player.ui.map +4 -4
  25. package/externs/device_sensor_event.js +18 -0
  26. package/externs/hisense.js +16 -0
  27. package/externs/playstation.js +31 -0
  28. package/externs/shaka/abr_manager.js +3 -1
  29. package/externs/shaka/ads.js +5 -0
  30. package/externs/shaka/codecs.js +13 -0
  31. package/externs/shaka/manifest.js +58 -6
  32. package/externs/shaka/net.js +13 -1
  33. package/externs/shaka/player.js +184 -13
  34. package/externs/shaka/text.js +19 -0
  35. package/externs/tizen.js +41 -0
  36. package/externs/webos.js +19 -0
  37. package/externs/xbox.js +43 -0
  38. package/lib/abr/simple_abr_manager.js +9 -1
  39. package/lib/ads/ad_manager.js +19 -2
  40. package/lib/ads/client_side_ad_manager.js +5 -3
  41. package/lib/ads/media_tailor_ad_manager.js +9 -0
  42. package/lib/cast/cast_receiver.js +0 -14
  43. package/lib/cast/cast_utils.js +1 -0
  44. package/lib/cea/mp4_cea_parser.js +6 -1
  45. package/lib/dash/content_protection.js +97 -53
  46. package/lib/dash/dash_parser.js +230 -176
  47. package/lib/dash/mpd_utils.js +72 -65
  48. package/lib/dash/segment_base.js +27 -21
  49. package/lib/dash/segment_list.js +18 -16
  50. package/lib/dash/segment_template.js +26 -21
  51. package/lib/hls/hls_parser.js +358 -164
  52. package/lib/media/buffering_observer.js +25 -1
  53. package/lib/media/drm_engine.js +91 -71
  54. package/lib/media/manifest_filterer.js +220 -0
  55. package/lib/media/media_source_engine.js +34 -5
  56. package/lib/media/preload_manager.js +827 -0
  57. package/lib/media/presentation_timeline.js +11 -0
  58. package/lib/media/segment_index.js +40 -30
  59. package/lib/media/segment_prefetch.js +140 -43
  60. package/lib/media/segment_reference.js +31 -8
  61. package/lib/media/segment_utils.js +208 -25
  62. package/lib/media/streaming_engine.js +319 -63
  63. package/lib/mss/content_protection.js +31 -25
  64. package/lib/mss/mss_parser.js +74 -55
  65. package/lib/net/networking_engine.js +49 -4
  66. package/lib/offline/storage.js +3 -3
  67. package/lib/player.js +882 -673
  68. package/lib/polyfill/media_capabilities.js +16 -1
  69. package/lib/text/simple_text_displayer.js +9 -0
  70. package/lib/text/stub_text_displayer.js +7 -0
  71. package/lib/text/text_engine.js +16 -3
  72. package/lib/text/ttml_text_parser.js +116 -114
  73. package/lib/text/ui_text_displayer.js +28 -11
  74. package/lib/text/vtt_text_parser.js +54 -115
  75. package/lib/transmuxer/opus.js +91 -0
  76. package/lib/transmuxer/ts_transmuxer.js +115 -0
  77. package/lib/util/cmcd_manager.js +152 -3
  78. package/lib/util/dom_utils.js +11 -0
  79. package/lib/util/error.js +5 -0
  80. package/lib/util/fake_event.js +2 -0
  81. package/lib/util/manifest_parser_utils.js +97 -3
  82. package/lib/util/mp4_box_parsers.js +346 -17
  83. package/lib/util/mp4_generator.js +23 -0
  84. package/lib/util/periods.js +19 -0
  85. package/lib/util/platform.js +161 -9
  86. package/lib/util/player_configuration.js +61 -3
  87. package/lib/util/stats.js +16 -0
  88. package/lib/util/stream_utils.js +16 -6
  89. package/lib/util/string_utils.js +36 -0
  90. package/lib/util/tXml.js +814 -0
  91. package/lib/util/ts_parser.js +50 -0
  92. package/lib/util/xml_utils.js +10 -338
  93. package/package.json +2 -2
  94. package/shaka-player.uncompiled.js +8 -0
  95. package/ui/controls.js +187 -5
  96. package/ui/enums.js +2 -0
  97. package/ui/externs/ui.js +30 -3
  98. package/ui/gl_matrix/matrix_4x4.js +568 -0
  99. package/ui/gl_matrix/matrix_quaternion.js +45 -0
  100. package/ui/less/containers.less +11 -0
  101. package/ui/locales/en-GB.json +2 -0
  102. package/ui/locales/en.json +2 -0
  103. package/ui/locales/es-419.json +2 -0
  104. package/ui/locales/es.json +2 -0
  105. package/ui/locales/source.json +8 -0
  106. package/ui/recenter_vr.js +126 -0
  107. package/ui/resolution_selection.js +25 -5
  108. package/ui/statistics_button.js +12 -0
  109. package/ui/text_selection.js +1 -1
  110. package/ui/toggle_stereoscopic.js +127 -0
  111. package/ui/ui.js +74 -38
  112. package/ui/vr_manager.js +557 -0
  113. package/ui/vr_utils.js +232 -0
  114. package/ui/vr_webgl.js +697 -0
@@ -15,7 +15,7 @@ goog.require('shaka.text.TextEngine');
15
15
  goog.require('shaka.util.Error');
16
16
  goog.require('shaka.util.StringUtils');
17
17
  goog.require('shaka.util.TextParser');
18
- goog.require('shaka.util.XmlUtils');
18
+ goog.require('shaka.util.TXml');
19
19
 
20
20
 
21
21
  /**
@@ -458,26 +458,34 @@ shaka.text.VttTextParser = class {
458
458
  */
459
459
  static parseCueStyles(payload, rootCue, styles) {
460
460
  const VttTextParser = shaka.text.VttTextParser;
461
+ const StringUtils = shaka.util.StringUtils;
462
+ const TXml = shaka.util.TXml;
461
463
  // Optimization for unstyled payloads.
462
464
  if (!payload.includes('<')) {
463
- rootCue.payload = VttTextParser.htmlUnescape_(payload);
465
+ rootCue.payload = StringUtils.htmlUnescape(payload);
464
466
  return;
465
467
  }
466
468
  if (styles.size === 0) {
467
469
  VttTextParser.addDefaultTextColor_(styles);
468
470
  }
469
- payload = VttTextParser.replaceColorPayload_(payload);
471
+
470
472
  payload = VttTextParser.replaceKaraokeStylePayload_(payload);
471
473
  payload = VttTextParser.replaceVoiceStylePayload_(payload);
474
+ payload = VttTextParser.escapeInvalidChevrons_(payload);
472
475
  const xmlPayload = '<span>' + payload + '</span>';
473
- const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
476
+ let element;
477
+ try {
478
+ element = TXml.parseXmlString(xmlPayload, 'span');
479
+ } catch (e) {
480
+ shaka.log.warning('cue parse fail: ', e);
481
+ }
482
+
474
483
  if (element) {
475
- const childNodes = element.childNodes;
484
+ const childNodes = element.children;
476
485
  if (childNodes.length == 1) {
477
486
  const childNode = childNodes[0];
478
- if (childNode.nodeType == Node.TEXT_NODE ||
479
- childNode.nodeType == Node.CDATA_SECTION_NODE) {
480
- rootCue.payload = VttTextParser.htmlUnescape_(payload);
487
+ if (!TXml.isNode(childNode)) {
488
+ rootCue.payload = StringUtils.htmlUnescape(payload);
481
489
  return;
482
490
  }
483
491
  }
@@ -486,8 +494,34 @@ shaka.text.VttTextParser = class {
486
494
  }
487
495
  } else {
488
496
  shaka.log.warning('The cue\'s markup could not be parsed: ', payload);
489
- rootCue.payload = VttTextParser.htmlUnescape_(payload);
497
+ rootCue.payload = StringUtils.htmlUnescape(payload);
498
+ }
499
+ }
500
+
501
+ /**
502
+ * This method converts invalid > chevrons to HTML entities.
503
+ * It also removes < chevrons as per spec.
504
+ *
505
+ * @param {!string} input
506
+ * @return {string}
507
+ * @private
508
+ */
509
+ static escapeInvalidChevrons_(input) {
510
+ // Used to map HTML entities to characters.
511
+ const htmlEscapes = {
512
+ '< ': '',
513
+ ' >': ' &gt;',
514
+ };
515
+
516
+ const reEscapedHtml = /(< +>|<\s|\s>)/g;
517
+ const reHasEscapedHtml = RegExp(reEscapedHtml.source);
518
+ // This check is an optimization, since replace always makes a copy
519
+ if (input && reHasEscapedHtml.test(input)) {
520
+ return input.replace(reEscapedHtml, (entity) => {
521
+ return htmlEscapes[entity] || '';
522
+ });
490
523
  }
524
+ return input || '';
491
525
  }
492
526
 
493
527
  /**
@@ -601,66 +635,6 @@ shaka.text.VttTextParser = class {
601
635
  return newPayload;
602
636
  }
603
637
 
604
- /**
605
- * Converts color end tag to be valid for xml parsing
606
- * For example,
607
- * input: <c.yellow.bg_blue>Yellow text on blue bg</c>
608
- * output: <c.yellow.bg_blue>Yellow text on blue bg</c.yellow.bg_blue>
609
- *
610
- * Returns original payload if invalid tag is found.
611
- * Invalid tag example: <c.yellow><b>Example</c></b>
612
- *
613
- * @param {string} payload
614
- * @return {string} processed payload
615
- * @private
616
- */
617
- static replaceColorPayload_(payload) {
618
- const names = [];
619
- let nameStart = -1;
620
- let newPayload = '';
621
- for (let i = 0; i < payload.length; i++) {
622
- if (payload[i] === '/' && i > 0 && payload[i - 1] === '<') {
623
- const end = payload.indexOf('>', i);
624
- if (end <= i) {
625
- return payload;
626
- }
627
- const tagEnd = payload.substring(i + 1, end);
628
- if (!tagEnd || tagEnd !== 'c') {
629
- newPayload += payload[i];
630
- continue;
631
- }
632
- const tagStart = names.pop();
633
- if (!tagStart) {
634
- newPayload += payload[i];
635
- } else if (tagStart === tagEnd) {
636
- newPayload += '/' + tagEnd + '>';
637
- i += tagEnd.length + 1;
638
- } else {
639
- if (!tagStart.startsWith('c.')) {
640
- newPayload += payload[i];
641
- continue;
642
- }
643
- i += tagEnd.length + 1;
644
- newPayload += '/' + tagStart + '>';
645
- }
646
- } else {
647
- if (payload[i] === '<') {
648
- nameStart = i + 1;
649
- if (payload[nameStart] != 'c') {
650
- nameStart = -1;
651
- }
652
- } else if (payload[i] === '>') {
653
- if (nameStart > 0) {
654
- names.push(payload.substr(nameStart, i - nameStart));
655
- nameStart = -1;
656
- }
657
- }
658
- newPayload += payload[i];
659
- }
660
- }
661
- return newPayload;
662
- }
663
-
664
638
  /**
665
639
  * @param {string} value
666
640
  * @param {string} defaultValue
@@ -708,13 +682,14 @@ shaka.text.VttTextParser = class {
708
682
  }
709
683
 
710
684
  /**
711
- * @param {!Node} element
685
+ * @param {!shaka.extern.xml.Node} element
712
686
  * @param {!shaka.text.Cue} rootCue
713
687
  * @param {!Map.<string, shaka.text.Cue>} styles
714
688
  * @private
715
689
  */
716
690
  static generateCueFromElement_(element, rootCue, styles) {
717
691
  const VttTextParser = shaka.text.VttTextParser;
692
+ const TXml = shaka.util.TXml;
718
693
  const nestedCue = rootCue.clone();
719
694
  // We don't want propagate some properties.
720
695
  nestedCue.nestedCues = [];
@@ -725,11 +700,12 @@ shaka.text.VttTextParser = class {
725
700
  nestedCue.region = new shaka.text.CueRegion();
726
701
  nestedCue.position = null;
727
702
  nestedCue.size = 0;
728
- if (element.nodeType === Node.ELEMENT_NODE && element.nodeName) {
703
+
704
+ if (shaka.util.TXml.isNode(element)) {
729
705
  const bold = shaka.text.Cue.fontWeight.BOLD;
730
706
  const italic = shaka.text.Cue.fontStyle.ITALIC;
731
707
  const underline = shaka.text.Cue.textDecoration.UNDERLINE;
732
- const tags = element.nodeName.split(/(?=[ .])+/g);
708
+ const tags = element.tagName.split(/(?=[ .])+/g);
733
709
  for (const tag of tags) {
734
710
  let styleTag = tag;
735
711
  // White blanks at start indicate that the style is a voice
@@ -762,15 +738,14 @@ shaka.text.VttTextParser = class {
762
738
  nestedCue.textDecoration.push(underline);
763
739
  break;
764
740
  case 'font': {
765
- const color =
766
- /** @type {!Element} */(element).getAttribute('color');
741
+ const color = element.attributes['color'];
767
742
  if (color) {
768
743
  nestedCue.color = color;
769
744
  }
770
745
  break;
771
746
  }
772
747
  case 'div': {
773
- const time = /** @type {!Element} */(element).getAttribute('time');
748
+ const time = element.attributes['time'];
774
749
  if (!time) {
775
750
  break;
776
751
  }
@@ -792,13 +767,13 @@ shaka.text.VttTextParser = class {
792
767
  }
793
768
  }
794
769
 
795
- const isTextNode = (item) => shaka.util.XmlUtils.isText(item);
796
- const childNodes = element.childNodes;
770
+ const isTextNode = (item) => shaka.util.TXml.isText(item);
771
+ const childNodes = element.children;
797
772
  if (isTextNode(element) ||
798
773
  (childNodes.length == 1 && isTextNode(childNodes[0]))) {
799
774
  // Trailing line breaks may lost when convert cue to HTML tag
800
775
  // Need to insert line break cue to preserve line breaks
801
- const textArr = element.textContent.split('\n');
776
+ const textArr = TXml.getTextContents(element).split('\n');
802
777
  let isFirst = true;
803
778
  for (const text of textArr) {
804
779
  if (!isFirst) {
@@ -808,7 +783,7 @@ shaka.text.VttTextParser = class {
808
783
  }
809
784
  if (text.length > 0) {
810
785
  const textCue = nestedCue.clone();
811
- textCue.payload = VttTextParser.htmlUnescape_(text);
786
+ textCue.payload = shaka.util.StringUtils.htmlUnescape(text);
812
787
  rootCue.nestedCues.push(textCue);
813
788
  }
814
789
  isFirst = false;
@@ -1024,42 +999,6 @@ shaka.text.VttTextParser = class {
1024
999
 
1025
1000
  return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
1026
1001
  }
1027
-
1028
- /**
1029
- * This method converts the HTML entities &amp;, &lt;, &gt;, &quot;, &#39;,
1030
- * &nbsp;, &lrm; and &rlm; in string to their corresponding characters.
1031
- *
1032
- * @param {!string} input
1033
- * @return {string}
1034
- * @private
1035
- */
1036
- static htmlUnescape_(input) {
1037
- // Used to map HTML entities to characters.
1038
- const htmlUnescapes = {
1039
- '&amp;': '&',
1040
- '&lt;': '<',
1041
- '&gt;': '>',
1042
- '&quot;': '"',
1043
- '&#39;': '\'',
1044
- '&nbsp;': '\u{a0}',
1045
- '&lrm;': '\u{200e}',
1046
- '&rlm;': '\u{200f}',
1047
- };
1048
-
1049
- // Used to match HTML entities and HTML characters.
1050
- const reEscapedHtml = /&(?:amp|lt|gt|quot|#(0+)?39|nbsp|lrm|rlm);/g;
1051
- const reHasEscapedHtml = RegExp(reEscapedHtml.source);
1052
- // This check is an optimization, since replace always makes a copy
1053
- if (input && reHasEscapedHtml.test(input)) {
1054
- return input.replace(reEscapedHtml, (entity) => {
1055
- // The only thing that might not match the dictionary above is the
1056
- // single quote, which can be matched by many strings in the regex, but
1057
- // only has a single entry in the dictionary.
1058
- return htmlUnescapes[entity] || '\'';
1059
- });
1060
- }
1061
- return input || '';
1062
- }
1063
1002
  };
1064
1003
 
1065
1004
  /**
@@ -0,0 +1,91 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.provide('shaka.transmuxer.Opus');
8
+
9
+ goog.requireType('shaka.util.TsParser');
10
+
11
+
12
+ /**
13
+ * Opus utils
14
+ */
15
+ shaka.transmuxer.Opus = class {
16
+ /**
17
+ * @param {!shaka.util.TsParser.OpusMetadata} metadata
18
+ * @return {!Uint8Array}
19
+ */
20
+ static getAudioConfig(metadata) {
21
+ let mapping = [];
22
+ switch (metadata.channelConfigCode) {
23
+ case 0x01:
24
+ case 0x02:
25
+ mapping = [0x0];
26
+ break;
27
+ case 0x00: // dualmono
28
+ mapping = [0xFF, 1, 1, 0, 1];
29
+ break;
30
+ case 0x80: // dualmono
31
+ mapping = [0xFF, 2, 0, 0, 1];
32
+ break;
33
+ case 0x03:
34
+ mapping = [0x01, 2, 1, 0, 2, 1];
35
+ break;
36
+ case 0x04:
37
+ mapping = [0x01, 2, 2, 0, 1, 2, 3];
38
+ break;
39
+ case 0x05:
40
+ mapping = [0x01, 3, 2, 0, 4, 1, 2, 3];
41
+ break;
42
+ case 0x06:
43
+ mapping = [0x01, 4, 2, 0, 4, 1, 2, 3, 5];
44
+ break;
45
+ case 0x07:
46
+ mapping = [0x01, 4, 2, 0, 4, 1, 2, 3, 5, 6];
47
+ break;
48
+ case 0x08:
49
+ mapping = [0x01, 5, 3, 0, 6, 1, 2, 3, 4, 5, 7];
50
+ break;
51
+ case 0x82:
52
+ mapping = [0x01, 1, 2, 0, 1];
53
+ break;
54
+ case 0x83:
55
+ mapping = [0x01, 1, 3, 0, 1, 2];
56
+ break;
57
+ case 0x84:
58
+ mapping = [0x01, 1, 4, 0, 1, 2, 3];
59
+ break;
60
+ case 0x85:
61
+ mapping = [0x01, 1, 5, 0, 1, 2, 3, 4];
62
+ break;
63
+ case 0x86:
64
+ mapping = [0x01, 1, 6, 0, 1, 2, 3, 4, 5];
65
+ break;
66
+ case 0x87:
67
+ mapping = [0x01, 1, 7, 0, 1, 2, 3, 4, 5, 6];
68
+ break;
69
+ case 0x88:
70
+ mapping = [0x01, 1, 8, 0, 1, 2, 3, 4, 5, 6, 7];
71
+ break;
72
+ }
73
+
74
+ return new Uint8Array([
75
+ 0x00, // Version (1)
76
+ metadata.channelCount, // OutputChannelCount: 2
77
+ 0x00, 0x00, // PreSkip: 2
78
+ (metadata.sampleRate >>> 24) & 0xFF, // Audio sample rate: 4
79
+ (metadata.sampleRate >>> 17) & 0xFF,
80
+ (metadata.sampleRate >>> 8) & 0xFF,
81
+ (metadata.sampleRate >>> 0) & 0xFF,
82
+ 0x00, 0x00, // Global Gain : 2
83
+ ...mapping,
84
+ ]);
85
+ }
86
+ };
87
+
88
+ /**
89
+ * @const {number}
90
+ */
91
+ shaka.transmuxer.Opus.OPUS_AUDIO_SAMPLE_PER_FRAME = 960;
@@ -14,6 +14,7 @@ goog.require('shaka.transmuxer.Ec3');
14
14
  goog.require('shaka.transmuxer.H264');
15
15
  goog.require('shaka.transmuxer.H265');
16
16
  goog.require('shaka.transmuxer.MpegAudio');
17
+ goog.require('shaka.transmuxer.Opus');
17
18
  goog.require('shaka.transmuxer.TransmuxerEngine');
18
19
  goog.require('shaka.util.BufferUtils');
19
20
  goog.require('shaka.util.Error');
@@ -274,6 +275,10 @@ shaka.transmuxer.TsTransmuxer = class {
274
275
  streamInfo =
275
276
  this.getMp3StreamInfo_(tsParser, stream, duration);
276
277
  break;
278
+ case 'opus':
279
+ streamInfo =
280
+ this.getOpusStreamInfo_(tsParser, stream, duration);
281
+ break;
277
282
  }
278
283
  if (streamInfo) {
279
284
  streamInfos.push(streamInfo);
@@ -734,6 +739,115 @@ shaka.transmuxer.TsTransmuxer = class {
734
739
  }
735
740
 
736
741
 
742
+ /**
743
+ * @param {shaka.util.TsParser} tsParser
744
+ * @param {shaka.extern.Stream} stream
745
+ * @param {number} duration
746
+ * @return {shaka.util.Mp4Generator.StreamInfo}
747
+ * @private
748
+ */
749
+ getOpusStreamInfo_(tsParser, stream, duration) {
750
+ const Opus = shaka.transmuxer.Opus;
751
+ const timescale = shaka.util.TsParser.Timescale;
752
+
753
+ /** @type {!Array.<shaka.util.Mp4Generator.Mp4Sample>} */
754
+ const samples = [];
755
+
756
+ let firstPts = null;
757
+
758
+ /** @type {?shaka.util.TsParser.OpusMetadata} */
759
+ const opusMetadata = tsParser.getOpusMetadata();
760
+
761
+ if (!opusMetadata) {
762
+ throw new shaka.util.Error(
763
+ shaka.util.Error.Severity.CRITICAL,
764
+ shaka.util.Error.Category.MEDIA,
765
+ shaka.util.Error.Code.TRANSMUXING_FAILED);
766
+ }
767
+
768
+ /** @type {!Uint8Array} */
769
+ const audioConfig = Opus.getAudioConfig(opusMetadata);
770
+
771
+ /** @type {number} */
772
+ const sampleRate = opusMetadata.sampleRate;
773
+
774
+ for (const audioData of tsParser.getAudioData()) {
775
+ const data = audioData.data;
776
+ if (firstPts == null && audioData.pts !== null) {
777
+ firstPts = audioData.pts;
778
+ }
779
+ let offset = 0;
780
+ while (offset < data.length) {
781
+ const opusPendingTrimStart = (data[offset + 1] & 0x10) !== 0;
782
+ const trimEnd = (data[offset + 1] & 0x08) !== 0;
783
+ let index = offset + 2;
784
+ let size = 0;
785
+
786
+ while (data[index] === 0xFF) {
787
+ size += 255;
788
+ index += 1;
789
+ }
790
+ size += data[index];
791
+ index += 1;
792
+ index += opusPendingTrimStart ? 2 : 0;
793
+ index += trimEnd ? 2 : 0;
794
+
795
+ const sample = data.slice(index, index + size);
796
+
797
+ samples.push({
798
+ data: sample,
799
+ size: sample.byteLength,
800
+ duration: Opus.OPUS_AUDIO_SAMPLE_PER_FRAME,
801
+ cts: 0,
802
+ flags: {
803
+ isLeading: 0,
804
+ isDependedOn: 0,
805
+ hasRedundancy: 0,
806
+ degradPrio: 0,
807
+ dependsOn: 2,
808
+ isNonSync: 0,
809
+ },
810
+ });
811
+ offset = index + size;
812
+ }
813
+ }
814
+
815
+ if (audioConfig.byteLength == 0 || firstPts == null) {
816
+ throw new shaka.util.Error(
817
+ shaka.util.Error.Severity.CRITICAL,
818
+ shaka.util.Error.Category.MEDIA,
819
+ shaka.util.Error.Code.TRANSMUXING_FAILED);
820
+ }
821
+
822
+ stream.audioSamplingRate = opusMetadata.sampleRate;
823
+ stream.channelsCount = opusMetadata.channelCount;
824
+
825
+
826
+ /** @type {number} */
827
+ const baseMediaDecodeTime = firstPts / timescale * sampleRate;
828
+
829
+ return {
830
+ id: stream.id,
831
+ type: shaka.util.ManifestParserUtils.ContentType.AUDIO,
832
+ codecs: 'opus',
833
+ encrypted: stream.encrypted && stream.drmInfos.length > 0,
834
+ timescale: sampleRate,
835
+ duration: duration,
836
+ videoNalus: [],
837
+ audioConfig: audioConfig,
838
+ videoConfig: new Uint8Array([]),
839
+ hSpacing: 0,
840
+ vSpacing: 0,
841
+ data: {
842
+ sequenceNumber: this.frameIndex_,
843
+ baseMediaDecodeTime: baseMediaDecodeTime,
844
+ samples: samples,
845
+ },
846
+ stream: stream,
847
+ };
848
+ }
849
+
850
+
737
851
  /**
738
852
  * @param {shaka.util.TsParser} tsParser
739
853
  * @param {shaka.extern.Stream} stream
@@ -933,6 +1047,7 @@ shaka.transmuxer.TsTransmuxer.SUPPORTED_AUDIO_CODECS_ = [
933
1047
  'ac-3',
934
1048
  'ec-3',
935
1049
  'mp3',
1050
+ 'opus',
936
1051
  ];
937
1052
 
938
1053
  /**
@@ -9,6 +9,9 @@ goog.provide('shaka.util.CmcdManager');
9
9
  goog.require('goog.Uri');
10
10
  goog.require('shaka.log');
11
11
  goog.require('shaka.net.NetworkingEngine');
12
+ goog.require('shaka.util.ArrayUtils');
13
+
14
+ goog.requireType('shaka.media.SegmentReference');
12
15
 
13
16
  /**
14
17
  * @summary
@@ -186,11 +189,49 @@ shaka.util.CmcdManager = class {
186
189
  if (stream) {
187
190
  if (isMedia) {
188
191
  data.bl = this.getBufferLength_(stream.type);
192
+ if (data.ot !== ObjectType.TIMED_TEXT) {
193
+ const remainingBufferLength =
194
+ this.getRemainingBufferLength_(stream.type);
195
+ const playbackRate = this.playerInterface_.getPlaybackRate();
196
+ if (playbackRate) {
197
+ data.dl = remainingBufferLength / Math.abs(playbackRate);
198
+ } else {
199
+ data.dl = remainingBufferLength;
200
+ }
201
+ }
189
202
  }
190
203
 
191
204
  if (stream.bandwidth) {
192
205
  data.br = stream.bandwidth / 1000;
193
206
  }
207
+
208
+ if (stream.segmentIndex && segment) {
209
+ const iterator = stream.segmentIndex.getIteratorForTime(
210
+ segment.endTime, /* allowNonIndepedent= */ true);
211
+ if (iterator) {
212
+ const nextSegment = iterator.next().value;
213
+ if (nextSegment && nextSegment != segment) {
214
+ if (!shaka.util.ArrayUtils.equal(
215
+ segment.getUris(), nextSegment.getUris())) {
216
+ data.nor = this.urlToRelativePath_(
217
+ nextSegment.getUris()[0], request.uris[0]);
218
+ }
219
+ if ((nextSegment.startByte || nextSegment.endByte) &&
220
+ (segment.startByte != nextSegment.startByte ||
221
+ segment.endByte != nextSegment.endByte)) {
222
+ let range = nextSegment.startByte + '-';
223
+ if (nextSegment.endByte) {
224
+ range += nextSegment.endByte;
225
+ }
226
+ data.nrr = range;
227
+ }
228
+ }
229
+ }
230
+ const rtp = this.calculateRtp_(stream, segment);
231
+ if (!isNaN(rtp)) {
232
+ data.rtp = rtp;
233
+ }
234
+ }
194
235
  }
195
236
 
196
237
  if (isMedia && data.ot !== ObjectType.TIMED_TEXT) {
@@ -328,17 +369,17 @@ shaka.util.CmcdManager = class {
328
369
  data.su = this.buffering_;
329
370
  }
330
371
 
331
- // TODO: Implement rtp, nrr, nor, dl
372
+ const output = this.filterKeys_(data);
332
373
 
333
374
  if (useHeaders) {
334
- const headers = shaka.util.CmcdManager.toHeaders(data);
375
+ const headers = shaka.util.CmcdManager.toHeaders(output);
335
376
  if (!Object.keys(headers).length) {
336
377
  return;
337
378
  }
338
379
 
339
380
  Object.assign(request.headers, headers);
340
381
  } else {
341
- const query = shaka.util.CmcdManager.toQuery(data);
382
+ const query = shaka.util.CmcdManager.toQuery(output);
342
383
  if (!query) {
343
384
  return;
344
385
  }
@@ -349,6 +390,29 @@ shaka.util.CmcdManager = class {
349
390
  }
350
391
  }
351
392
 
393
+ /**
394
+ * Filter the CMCD data object to include only the keys specified in the
395
+ * configuration.
396
+ *
397
+ * @param {CmcdData} data
398
+ * @return {CmcdData}
399
+ * @private
400
+ */
401
+ filterKeys_(data) {
402
+ const includeKeys = this.config_.includeKeys;
403
+
404
+ if (!includeKeys.length) {
405
+ return data;
406
+ }
407
+
408
+ return Object.keys(data).reduce((acc, key) => {
409
+ if (includeKeys.includes(key)) {
410
+ acc[key] = data[key];
411
+ }
412
+ return acc;
413
+ }, {});
414
+ }
415
+
352
416
  /**
353
417
  * The CMCD object type.
354
418
  *
@@ -451,6 +515,91 @@ shaka.util.CmcdManager = class {
451
515
  return (range.end - start) * 1000;
452
516
  }
453
517
 
518
+ /**
519
+ * Get the remaining buffer length for a media type in milliseconds
520
+ *
521
+ * @param {string} type
522
+ * @return {number}
523
+ * @private
524
+ */
525
+ getRemainingBufferLength_(type) {
526
+ const ranges = this.playerInterface_.getBufferedInfo()[type];
527
+
528
+ if (!ranges.length) {
529
+ return 0;
530
+ }
531
+
532
+ const start = this.playerInterface_.getCurrentTime();
533
+ const range = ranges.find((r) => r.start <= start && r.end >= start);
534
+
535
+ if (!range) {
536
+ return 0;
537
+ }
538
+
539
+ return (range.end - start) * 1000;
540
+ }
541
+
542
+ /**
543
+ * Constructs a relative path from a URL
544
+ *
545
+ * @param {string} url
546
+ * @param {string} base
547
+ * @return {string}
548
+ * @private
549
+ */
550
+ urlToRelativePath_(url, base) {
551
+ const to = new URL(url);
552
+ const from = new URL(base);
553
+
554
+ if (to.origin !== from.origin) {
555
+ return url;
556
+ }
557
+
558
+ const toPath = to.pathname.split('/').slice(1);
559
+ const fromPath = from.pathname.split('/').slice(1, -1);
560
+
561
+ // remove common parents
562
+ while (toPath[0] === fromPath[0]) {
563
+ toPath.shift();
564
+ fromPath.shift();
565
+ }
566
+
567
+ // add back paths
568
+ while (fromPath.length) {
569
+ fromPath.shift();
570
+ toPath.unshift('..');
571
+ }
572
+
573
+ return toPath.join('/');
574
+ }
575
+
576
+ /**
577
+ * Calculate requested maximun throughput
578
+ *
579
+ * @param {shaka.extern.Stream} stream
580
+ * @param {shaka.media.SegmentReference} segment
581
+ * @return {number}
582
+ * @private
583
+ */
584
+ calculateRtp_(stream, segment) {
585
+ const playbackRate = this.playerInterface_.getPlaybackRate() || 1;
586
+ const currentBufferLevel =
587
+ this.getRemainingBufferLength_(stream.type) || 500;
588
+ const bandwidth = stream.bandwidth;
589
+ if (!bandwidth) {
590
+ return NaN;
591
+ }
592
+ const segmentDuration = segment.endTime - segment.startTime;
593
+ // Calculate file size in kilobits
594
+ const segmentSize = bandwidth * segmentDuration / 1000;
595
+ // Calculate time available to load file in seconds
596
+ const timeToLoad = (currentBufferLevel / playbackRate) / 1000;
597
+ // Calculate the exact bandwidth required
598
+ const minBandwidth = segmentSize / timeToLoad;
599
+ // Include a safety buffer
600
+ return minBandwidth * this.config_.rtpSafetyFactor;
601
+ }
602
+
454
603
  /**
455
604
  * Get the stream format
456
605
  *