shaka-player 3.2.0 → 3.2.4

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 (162) hide show
  1. package/.github/workflows/build_and_test.yaml +73 -0
  2. package/.github/workflows/release.yaml +53 -0
  3. package/AUTHORS +3 -0
  4. package/CHANGELOG.md +182 -1
  5. package/CONTRIBUTORS +3 -0
  6. package/README.md +4 -4
  7. package/build/checkversion.py +1 -1
  8. package/build/generateExterns.js +1 -1
  9. package/build/misspellings.txt +1 -0
  10. package/build/shakaBuildHelpers.py +6 -5
  11. package/build/subprocessWindowsPatch.py +103 -0
  12. package/build/test.py +6 -0
  13. package/demo/common/assets.js +1 -1
  14. package/demo/index.html +1 -1
  15. package/dist/controls.css +2 -2
  16. package/dist/controls.css.map +1 -1
  17. package/dist/demo.compiled.debug.js +1 -1
  18. package/dist/demo.compiled.js +1 -1
  19. package/dist/deps.js +8 -8
  20. package/dist/locales.js +37 -37
  21. package/dist/receiver.compiled.debug.js +1 -1
  22. package/dist/receiver.compiled.js +1 -1
  23. package/dist/shaka-player.compiled.d.ts +195 -5
  24. package/dist/shaka-player.compiled.debug.d.ts +195 -5
  25. package/dist/shaka-player.compiled.debug.externs.js +226 -14
  26. package/dist/shaka-player.compiled.debug.js +424 -399
  27. package/dist/shaka-player.compiled.debug.map +4 -4
  28. package/dist/shaka-player.compiled.externs.js +226 -14
  29. package/dist/shaka-player.compiled.js +826 -812
  30. package/dist/shaka-player.compiled.map +4 -4
  31. package/dist/shaka-player.ui.d.ts +197 -16
  32. package/dist/shaka-player.ui.debug.d.ts +197 -16
  33. package/dist/shaka-player.ui.debug.externs.js +230 -27
  34. package/dist/shaka-player.ui.debug.js +466 -442
  35. package/dist/shaka-player.ui.debug.map +4 -4
  36. package/dist/shaka-player.ui.externs.js +230 -27
  37. package/dist/shaka-player.ui.js +946 -932
  38. package/dist/shaka-player.ui.map +4 -4
  39. package/docs/tutorials/fairplay.md +1 -1
  40. package/docs/tutorials/faq.md +14 -0
  41. package/docs/tutorials/ui.md +5 -5
  42. package/externs/ima.js +4 -0
  43. package/externs/shaka/ads.js +25 -0
  44. package/externs/shaka/player.js +8 -0
  45. package/externs/shaka/text.js +10 -0
  46. package/karma.conf.js +20 -15
  47. package/lib/ads/ad_manager.js +46 -26
  48. package/lib/ads/client_side_ad_manager.js +82 -88
  49. package/lib/ads/server_side_ad_manager.js +45 -13
  50. package/lib/cast/cast_proxy.js +6 -2
  51. package/lib/cast/cast_receiver.js +5 -1
  52. package/lib/cast/cast_sender.js +2 -2
  53. package/lib/cea/sei_processor.js +24 -17
  54. package/lib/dash/dash_parser.js +131 -24
  55. package/lib/dash/segment_list.js +1 -1
  56. package/lib/dash/segment_template.js +1 -1
  57. package/lib/hls/hls_parser.js +74 -17
  58. package/lib/hls/manifest_text_parser.js +1 -0
  59. package/lib/media/closed_caption_parser.js +3 -1
  60. package/lib/media/content_workarounds.js +12 -0
  61. package/lib/media/drm_engine.js +20 -1
  62. package/lib/media/gap_jumping_controller.js +8 -4
  63. package/lib/media/media_source_engine.js +1 -2
  64. package/lib/media/playhead.js +1 -1
  65. package/lib/media/region_observer.js +39 -33
  66. package/lib/media/region_timeline.js +17 -19
  67. package/lib/media/segment_index.js +2 -1
  68. package/lib/media/segment_reference.js +19 -1
  69. package/lib/media/streaming_engine.js +8 -4
  70. package/lib/net/networking_engine.js +6 -1
  71. package/lib/offline/download_manager.js +1 -2
  72. package/lib/offline/storage.js +8 -9
  73. package/lib/player.js +129 -59
  74. package/lib/polyfill/aria.js +12 -2
  75. package/lib/polyfill/encryption_scheme.js +2 -0
  76. package/lib/polyfill/fullscreen.js +2 -0
  77. package/lib/polyfill/mathround.js +2 -0
  78. package/lib/polyfill/media_capabilities.js +73 -21
  79. package/lib/polyfill/mediasource.js +8 -0
  80. package/lib/polyfill/orientation.js +3 -1
  81. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  82. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  83. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  84. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  85. package/lib/polyfill/pip_webkit.js +2 -0
  86. package/lib/polyfill/storage_estimate.js +2 -0
  87. package/lib/polyfill/video_play_promise.js +2 -0
  88. package/lib/polyfill/videoplaybackquality.js +2 -0
  89. package/lib/polyfill/vttcue.js +2 -0
  90. package/lib/text/cue.js +6 -0
  91. package/lib/text/mp4_ttml_parser.js +3 -0
  92. package/lib/text/text_engine.js +14 -2
  93. package/lib/text/ttml_text_parser.js +135 -87
  94. package/lib/text/ui_text_displayer.js +213 -80
  95. package/lib/text/vtt_text_parser.js +4 -2
  96. package/lib/text/web_vtt_generator.js +21 -12
  97. package/lib/util/fake_event.js +53 -7
  98. package/lib/util/fake_event_target.js +21 -1
  99. package/lib/util/mp4_box_parsers.js +1 -1
  100. package/lib/util/periods.js +4 -1
  101. package/lib/util/platform.js +13 -1
  102. package/lib/util/stream_utils.js +105 -13
  103. package/lib/util/xml_utils.js +50 -12
  104. package/package.json +3 -6
  105. package/roadmap.md +24 -22
  106. package/test/cast/cast_proxy_unit.js +12 -6
  107. package/test/cast/cast_receiver_integration.js +1 -1
  108. package/test/cast/cast_utils_unit.js +1 -1
  109. package/test/cea/mp4_cea_parser_unit.js +26 -0
  110. package/test/dash/dash_parser_live_unit.js +43 -0
  111. package/test/dash/dash_parser_manifest_unit.js +222 -3
  112. package/test/hls/hls_parser_unit.js +79 -1
  113. package/test/media/closed_caption_parser_unit.js +20 -0
  114. package/test/media/drm_engine_integration.js +86 -2
  115. package/test/media/drm_engine_unit.js +57 -0
  116. package/test/media/playhead_unit.js +27 -1
  117. package/test/media/region_observer_unit.js +40 -5
  118. package/test/media/region_timeline_unit.js +40 -22
  119. package/test/media/streaming_engine_integration.js +2 -1
  120. package/test/media/streaming_engine_unit.js +145 -0
  121. package/test/player_integration.js +79 -8
  122. package/test/player_src_equals_integration.js +41 -8
  123. package/test/player_unit.js +14 -4
  124. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  125. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  126. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  127. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  128. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  129. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  130. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  131. package/test/test/assets/3675/dash_0.mpd +30 -0
  132. package/test/test/assets/3675/dash_1.mpd +30 -0
  133. package/test/test/assets/3675/dash_2.mpd +30 -0
  134. package/test/test/assets/3675/dash_3.mpd +30 -0
  135. package/test/test/assets/3675/dash_4.mpd +30 -0
  136. package/test/test/assets/3675/dash_5.mpd +26 -0
  137. package/test/test/assets/3675/init-stream0.m4s +0 -0
  138. package/test/test/assets/chapters.vtt +2 -2
  139. package/test/test/assets/chapters2.vtt +10 -0
  140. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  141. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  142. package/test/test/util/fake_ad_manager.js +11 -1
  143. package/test/test/util/ttml_utils.js +81 -0
  144. package/test/test/util/util.js +10 -7
  145. package/test/text/mp4_ttml_parser_unit.js +11 -18
  146. package/test/text/ttml_text_parser_unit.js +542 -220
  147. package/test/text/ui_text_displayer_unit.js +93 -20
  148. package/test/text/vtt_text_parser_unit.js +17 -1
  149. package/test/text/web_vtt_generator_unit.js +58 -3
  150. package/test/ui/localization_unit.js +15 -14
  151. package/test/ui/text_displayer_layout_unit.js +21 -8
  152. package/test/util/mp4_box_parsers_unit.js +28 -0
  153. package/test/util/stream_utils_unit.js +60 -3
  154. package/ui/cast_button.js +2 -3
  155. package/ui/controls.js +11 -6
  156. package/ui/less/buttons.less +0 -4
  157. package/ui/less/containers.less +6 -7
  158. package/ui/locales/ru.json +6 -6
  159. package/ui/localization.js +40 -87
  160. package/ui/pip_button.js +2 -3
  161. package/ui/seek_bar.js +1 -2
  162. package/ui/text_selection.js +1 -0
@@ -95,6 +95,18 @@ shaka.dash.DashParser = class {
95
95
  /** @private {!shaka.util.OperationManager} */
96
96
  this.operationManager_ = new shaka.util.OperationManager();
97
97
 
98
+ /**
99
+ * Largest period start time seen.
100
+ * @private {?number}
101
+ */
102
+ this.largestPeriodStartTime_ = null;
103
+
104
+ /**
105
+ * Period IDs seen in previous manifest.
106
+ * @private {!Array.<number>}
107
+ */
108
+ this.lastManifestUpdatePeriodIds_ = [];
109
+
98
110
  /**
99
111
  * The minimum of the availabilityTimeOffset values among the adaptation
100
112
  * sets.
@@ -389,6 +401,24 @@ shaka.dash.DashParser = class {
389
401
  this.config_.dash.autoCorrectDrift);
390
402
  }
391
403
 
404
+ presentationTimeline.setStatic(mpdType == 'static');
405
+
406
+ const isLive = presentationTimeline.isLive();
407
+
408
+ // If it's live, we check for an override.
409
+ if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
410
+ segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
411
+ }
412
+
413
+ // If it's null, that means segments are always available. This is always
414
+ // the case for VOD, and sometimes the case for live.
415
+ if (segmentAvailabilityDuration == null) {
416
+ segmentAvailabilityDuration = Infinity;
417
+ }
418
+
419
+ presentationTimeline.setSegmentAvailabilityDuration(
420
+ segmentAvailabilityDuration);
421
+
392
422
  const profiles = mpd.getAttribute('profiles') || '';
393
423
 
394
424
  /** @type {shaka.dash.DashParser.Context} */
@@ -411,7 +441,6 @@ shaka.dash.DashParser = class {
411
441
  const duration = periodsAndDuration.duration;
412
442
  const periods = periodsAndDuration.periods;
413
443
 
414
- presentationTimeline.setStatic(mpdType == 'static');
415
444
  if (mpdType == 'static' ||
416
445
  !periodsAndDuration.durationDerivedFromPeriods) {
417
446
  // Ignore duration calculated from Period lengths if this is dynamic.
@@ -430,6 +459,7 @@ shaka.dash.DashParser = class {
430
459
  this.lowLatencyMode_ = this.playerInterface_.isLowLatencyMode();
431
460
  }
432
461
  }
462
+
433
463
  if (this.lowLatencyMode_) {
434
464
  presentationTimeline.setAvailabilityTimeOffset(
435
465
  this.minTotalAvailabilityTimeOffset_);
@@ -443,22 +473,6 @@ shaka.dash.DashParser = class {
443
473
  'https://bit.ly/3clctcj for details.');
444
474
  }
445
475
 
446
- const isLive = presentationTimeline.isLive();
447
-
448
- // If it's live, we check for an override.
449
- if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
450
- segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
451
- }
452
-
453
- // If it's null, that means segments are always available. This is always
454
- // the case for VOD, and sometimes the case for live.
455
- if (segmentAvailabilityDuration == null) {
456
- segmentAvailabilityDuration = Infinity;
457
- }
458
-
459
- presentationTimeline.setSegmentAvailabilityDuration(
460
- segmentAvailabilityDuration);
461
-
462
476
  // Use @maxSegmentDuration to override smaller, derived values.
463
477
  presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
464
478
  if (goog.DEBUG) {
@@ -538,6 +552,7 @@ shaka.dash.DashParser = class {
538
552
  for (const {i, item: elem, next} of enumerate(periodNodes)) {
539
553
  const start = /** @type {number} */ (
540
554
  XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
555
+ const periodId = elem.id;
541
556
  const givenDuration =
542
557
  XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
543
558
 
@@ -571,6 +586,42 @@ shaka.dash.DashParser = class {
571
586
  periodDuration = givenDuration;
572
587
  }
573
588
 
589
+ /**
590
+ * This is to improve robustness when the player observes manifest with
591
+ * past periods that are inconsistent to previous ones.
592
+ *
593
+ * This may happen when a CDN or proxy server switches its upstream from
594
+ * one encoder to another redundant encoder.
595
+ *
596
+ * Skip periods that match all of the following criteria:
597
+ * - Start time is earlier than latest period start time ever seen
598
+ * - Period ID is never seen in the previous manifest
599
+ * - Not the last period in the manifest
600
+ *
601
+ * Periods that meet the aforementioned criteria are considered invalid
602
+ * and should be safe to discard.
603
+ */
604
+
605
+ if (this.largestPeriodStartTime_ !== null &&
606
+ periodId !== null && start !== null &&
607
+ start < this.largestPeriodStartTime_ &&
608
+ !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
609
+ i + 1 != periodNodes.length) {
610
+ shaka.log.debug(
611
+ `Skipping Period with ID ${periodId} as its start time is smaller` +
612
+ ' than the largest period start time that has been seen, and ID ' +
613
+ 'is unseen before');
614
+ continue;
615
+ }
616
+
617
+
618
+ // Save maximum period start time if it is the last period
619
+ if (start !== null &&
620
+ (this.largestPeriodStartTime_ === null ||
621
+ start > this.largestPeriodStartTime_)) {
622
+ this.largestPeriodStartTime_ = start;
623
+ }
624
+
574
625
  // Parse child nodes.
575
626
  const info = {
576
627
  start: start,
@@ -602,6 +653,9 @@ shaka.dash.DashParser = class {
602
653
  prevEnd = start + periodDuration;
603
654
  } // end of period parsing loop
604
655
 
656
+ // Replace previous seen periods with the current one.
657
+ this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
658
+
605
659
  if (presentationDuration != null) {
606
660
  if (prevEnd != presentationDuration) {
607
661
  shaka.log.warning(
@@ -653,8 +707,12 @@ shaka.dash.DashParser = class {
653
707
 
654
708
  const eventStreamNodes =
655
709
  XmlUtils.findChildren(periodInfo.node, 'EventStream');
710
+ const availabilityStart =
711
+ context.presentationTimeline.getSegmentAvailabilityStart();
712
+
656
713
  for (const node of eventStreamNodes) {
657
- this.parseEventStream_(periodInfo.start, periodInfo.duration, node);
714
+ this.parseEventStream_(
715
+ periodInfo.start, periodInfo.duration, node, availabilityStart);
658
716
  }
659
717
 
660
718
  const adaptationSetNodes =
@@ -813,6 +871,25 @@ shaka.dash.DashParser = class {
813
871
  }
814
872
  }
815
873
 
874
+ // Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
875
+ let videoRange;
876
+ const videoRangeScheme = 'urn:mpeg:mpegB:cicp:TransferCharacteristics';
877
+ const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
878
+ switch (cicp) {
879
+ case 1:
880
+ case 6:
881
+ case 13:
882
+ case 14:
883
+ case 15:
884
+ return 'SDR';
885
+ case 16:
886
+ return 'PQ';
887
+ case 18:
888
+ return 'HLG';
889
+ }
890
+ return undefined;
891
+ };
892
+
816
893
  const essentialProperties =
817
894
  XmlUtils.findChildren(elem, 'EssentialProperty');
818
895
  // ID of real AdaptationSet if this is a trick mode set:
@@ -822,11 +899,26 @@ shaka.dash.DashParser = class {
822
899
  const schemeId = prop.getAttribute('schemeIdUri');
823
900
  if (schemeId == 'http://dashif.org/guidelines/trickmode') {
824
901
  trickModeFor = prop.getAttribute('value');
902
+ } else if (schemeId == videoRangeScheme) {
903
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
904
+ parseInt(prop.getAttribute('value'), 10),
905
+ );
825
906
  } else {
826
907
  unrecognizedEssentialProperty = true;
827
908
  }
828
909
  }
829
910
 
911
+ const supplementalProperties =
912
+ XmlUtils.findChildren(elem, 'SupplementalProperty');
913
+ for (const prop of supplementalProperties) {
914
+ const schemeId = prop.getAttribute('schemeIdUri');
915
+ if (schemeId == videoRangeScheme) {
916
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
917
+ parseInt(prop.getAttribute('value'), 10),
918
+ );
919
+ }
920
+ }
921
+
830
922
  const accessibilities = XmlUtils.findChildren(elem, 'Accessibility');
831
923
  const LanguageUtils = shaka.util.LanguageUtils;
832
924
  const closedCaptions = new Map();
@@ -863,7 +955,9 @@ shaka.dash.DashParser = class {
863
955
  channel = channelAndLanguage[0].startsWith('CC') ?
864
956
  channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
865
957
 
866
- language = channelAndLanguage[1]; // 3 letters (ISO 639-2)
958
+ // 3 letters (ISO 639-2). In b/187442669, we saw a blank string
959
+ // (CC2=;CC3=), so default to "und" (the code for "undetermined").
960
+ language = channelAndLanguage[1] || 'und';
867
961
  }
868
962
  closedCaptions.set(channel, LanguageUtils.normalize(language));
869
963
  }
@@ -950,8 +1044,13 @@ shaka.dash.DashParser = class {
950
1044
  // Parse Representations into Streams.
951
1045
  const representations = XmlUtils.findChildren(elem, 'Representation');
952
1046
  const streams = representations.map((representation) => {
953
- return this.parseRepresentation_(context, contentProtection, kind,
954
- language, label, main, roleValues, closedCaptions, representation);
1047
+ const parsedRepresentation = this.parseRepresentation_(context,
1048
+ contentProtection, kind, language, label, main, roleValues,
1049
+ closedCaptions, representation);
1050
+ if (parsedRepresentation) {
1051
+ parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange;
1052
+ }
1053
+ return parsedRepresentation;
955
1054
  }).filter((s) => !!s);
956
1055
 
957
1056
  if (streams.length == 0) {
@@ -1127,8 +1226,10 @@ shaka.dash.DashParser = class {
1127
1226
 
1128
1227
  let forced = false;
1129
1228
  if (isText) {
1130
- // See: https://github.com/google/shaka-player/issues/2122
1131
- forced = roles.includes('forced_subtitle');
1229
+ // See: https://github.com/google/shaka-player/issues/2122 and
1230
+ // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/165
1231
+ forced = roles.includes('forced_subtitle') ||
1232
+ roles.includes('forced-subtitle');
1132
1233
  }
1133
1234
 
1134
1235
  let tilesLayout;
@@ -1642,9 +1743,10 @@ shaka.dash.DashParser = class {
1642
1743
  * @param {number} periodStart
1643
1744
  * @param {?number} periodDuration
1644
1745
  * @param {!Element} elem
1746
+ * @param {number} availabilityStart
1645
1747
  * @private
1646
1748
  */
1647
- parseEventStream_(periodStart, periodDuration, elem) {
1749
+ parseEventStream_(periodStart, periodDuration, elem, availabilityStart) {
1648
1750
  const XmlUtils = shaka.util.XmlUtils;
1649
1751
  const parseNumber = XmlUtils.parseNonNegativeInt;
1650
1752
 
@@ -1667,6 +1769,11 @@ shaka.dash.DashParser = class {
1667
1769
  endTime = Math.min(endTime, periodStart + periodDuration);
1668
1770
  }
1669
1771
 
1772
+ // Don't add unavailable regions to the timeline.
1773
+ if (endTime < availabilityStart) {
1774
+ continue;
1775
+ }
1776
+
1670
1777
  /** @type {shaka.extern.TimelineRegionInfo} */
1671
1778
  const region = {
1672
1779
  schemeIdUri: schemeIdUri,
@@ -63,12 +63,12 @@ shaka.dash.SegmentList = class {
63
63
  const start = context.presentationTimeline.getSegmentAvailabilityStart();
64
64
  segmentIndex.mergeAndEvict(references, start);
65
65
  } else {
66
- context.presentationTimeline.notifySegments(references);
67
66
  segmentIndex = new shaka.media.SegmentIndex(references);
68
67
  if (id && context.dynamic) {
69
68
  segmentIndexMap[id] = segmentIndex;
70
69
  }
71
70
  }
71
+ context.presentationTimeline.notifySegments(references);
72
72
 
73
73
  if (!context.dynamic || !context.periodInfo.isLastPeriod) {
74
74
  const periodStart = context.periodInfo.start;
@@ -116,12 +116,12 @@ shaka.dash.SegmentTemplate = class {
116
116
  segmentIndex.mergeAndEvict(references,
117
117
  context.presentationTimeline.getSegmentAvailabilityStart());
118
118
  } else {
119
- context.presentationTimeline.notifySegments(references);
120
119
  segmentIndex = new shaka.media.SegmentIndex(references);
121
120
  if (id && context.dynamic) {
122
121
  segmentIndexMap[id] = segmentIndex;
123
122
  }
124
123
  }
124
+ context.presentationTimeline.notifySegments(references);
125
125
 
126
126
  if (shouldFit) {
127
127
  segmentIndex.fit(periodStart, periodEnd);
@@ -418,18 +418,16 @@ shaka.hls.HlsParser = class {
418
418
  const uri = tag.getAttributeValue('URI');
419
419
  const language = tag.getAttributeValue('LANGUAGE');
420
420
  const value = tag.getAttributeValue('VALUE');
421
- const data = {
422
- 'id': id,
423
- };
421
+ const data = (new Map()).set('id', id);
424
422
  if (uri) {
425
- data.uri =
426
- shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri);
423
+ data.set('uri',
424
+ shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri));
427
425
  }
428
426
  if (language) {
429
- data.language = language;
427
+ data.set('language', language);
430
428
  }
431
429
  if (value) {
432
- data.value = value;
430
+ data.set('value', value);
433
431
  }
434
432
  const event = new shaka.util.FakeEvent('sessiondata', data);
435
433
  if (this.playerInterface_) {
@@ -534,6 +532,30 @@ shaka.hls.HlsParser = class {
534
532
  }
535
533
  }
536
534
 
535
+ // Now that the content has been fit, notify segments.
536
+ this.segmentsToNotifyByStream_ = [];
537
+ const streamsToNotify = [];
538
+ for (const variant of variants) {
539
+ for (const stream of [variant.video, variant.audio]) {
540
+ if (stream) {
541
+ streamsToNotify.push(stream);
542
+ }
543
+ }
544
+ }
545
+ await Promise.all(streamsToNotify.map(async (stream) => {
546
+ await stream.createSegmentIndex();
547
+ }));
548
+ for (const stream of streamsToNotify) {
549
+ this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
550
+ }
551
+ this.notifySegments_();
552
+
553
+ // This asserts that the live edge is being calculated from segment times.
554
+ // For VOD and event streams, this check should still pass.
555
+ goog.asserts.assert(
556
+ !this.presentationTimeline_.usingPresentationStartTime(),
557
+ 'We should not be using the presentation start time in HLS!');
558
+
537
559
  this.manifest_ = {
538
560
  presentationTimeline: this.presentationTimeline_,
539
561
  variants,
@@ -1184,7 +1206,7 @@ shaka.hls.HlsParser = class {
1184
1206
  // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
1185
1207
  // only be selected explicitly by the user, and we don't have a way to
1186
1208
  // represent that in our model.
1187
- const defaultAttrValue = tag.getAttribute('DEFAULT');
1209
+ const defaultAttrValue = tag.getAttributeValue('DEFAULT');
1188
1210
  const primary = defaultAttrValue == 'YES';
1189
1211
 
1190
1212
  const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
@@ -1258,6 +1280,30 @@ shaka.hls.HlsParser = class {
1258
1280
  if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
1259
1281
  return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
1260
1282
  }
1283
+
1284
+ // Parse misc attributes.
1285
+ const resolution = tag.getAttributeValue('RESOLUTION');
1286
+ if (resolution) {
1287
+ // The RESOLUTION tag represents the resolution of a single thumbnail, not
1288
+ // of the entire sheet at once (like we expect in the output).
1289
+ // So multiply by the layout size.
1290
+
1291
+ const reference = streamInfo.stream.segmentIndex.get(0);
1292
+ const layout = reference.getTilesLayout();
1293
+ if (layout) {
1294
+ streamInfo.stream.width =
1295
+ Number(resolution.split('x')[0]) * Number(layout.split('x')[0]);
1296
+ streamInfo.stream.height =
1297
+ Number(resolution.split('x')[1]) * Number(layout.split('x')[1]);
1298
+ // TODO: What happens if there are multiple grids, with different
1299
+ // layout sizes, inside this image stream?
1300
+ }
1301
+ }
1302
+ const bandwidth = tag.getAttributeValue('BANDWIDTH');
1303
+ if (bandwidth) {
1304
+ streamInfo.stream.bandwidth = Number(bandwidth);
1305
+ }
1306
+
1261
1307
  this.uriToStreamInfosMap_.set(verbatimImagePlaylistUri, streamInfo);
1262
1308
  return streamInfo;
1263
1309
  }
@@ -1617,12 +1663,6 @@ shaka.hls.HlsParser = class {
1617
1663
  }
1618
1664
 
1619
1665
  this.notifySegments_();
1620
-
1621
- // This asserts that the live edge is being calculated from segment times.
1622
- // For VOD and event streams, this check should still pass.
1623
- goog.asserts.assert(
1624
- !this.presentationTimeline_.usingPresentationStartTime(),
1625
- 'We should not be using the presentation start time in HLS!');
1626
1666
  }
1627
1667
 
1628
1668
  /**
@@ -1795,6 +1835,7 @@ shaka.hls.HlsParser = class {
1795
1835
  }
1796
1836
 
1797
1837
  let tilesLayout = '';
1838
+ let tileDuration = null;
1798
1839
  if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
1799
1840
  // By default in HLS the tilesLayout is 1x1
1800
1841
  tilesLayout = '1x1';
@@ -1802,6 +1843,10 @@ shaka.hls.HlsParser = class {
1802
1843
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-TILES');
1803
1844
  if (tilesTag) {
1804
1845
  tilesLayout = tilesTag.getRequiredAttrValue('LAYOUT');
1846
+ const duration = tilesTag.getAttributeValue('DURATION');
1847
+ if (duration) {
1848
+ tileDuration = Number(duration);
1849
+ }
1805
1850
  }
1806
1851
  }
1807
1852
 
@@ -1817,6 +1862,7 @@ shaka.hls.HlsParser = class {
1817
1862
  /* appendWindowEnd= */ Infinity,
1818
1863
  partialSegmentRefs,
1819
1864
  tilesLayout,
1865
+ tileDuration,
1820
1866
  );
1821
1867
  }
1822
1868
 
@@ -2009,9 +2055,6 @@ shaka.hls.HlsParser = class {
2009
2055
  }
2010
2056
  }
2011
2057
 
2012
- this.segmentsToNotifyByStream_.push(references);
2013
- this.notifySegments_();
2014
-
2015
2058
  return references;
2016
2059
  }
2017
2060
 
@@ -2350,6 +2393,12 @@ shaka.hls.HlsParser = class {
2350
2393
  reader.rewind(1);
2351
2394
  };
2352
2395
 
2396
+ // We will look a few packet-lengths forward to find the first sync byte.
2397
+ // Note that we are using this method on what is already a subset of the
2398
+ // file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so
2399
+ // we can't look too far ahead to begin with.
2400
+ let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188);
2401
+
2353
2402
  // TODO: refactor this while loop for better readability.
2354
2403
  // eslint-disable-next-line no-constant-condition
2355
2404
  while (true) {
@@ -2358,8 +2407,16 @@ shaka.hls.HlsParser = class {
2358
2407
 
2359
2408
  syncByte = reader.readUint8();
2360
2409
  if (syncByte != 0x47) {
2410
+ if (syncByteScanLength > 0) {
2411
+ // This file could have started with a cut-off TS packet. Scan forward
2412
+ // until we find a sync byte.
2413
+ syncByteScanLength -= 1;
2414
+ continue;
2415
+ }
2361
2416
  fail();
2362
2417
  }
2418
+ // If we've found a sync byte, stop scanning forward for future packets.
2419
+ syncByteScanLength = 0;
2363
2420
 
2364
2421
  const flagsAndPacketId = reader.readUint16();
2365
2422
  const packetId = flagsAndPacketId & 0x1fff;
@@ -277,6 +277,7 @@ shaka.hls.ManifestTextParser = class {
277
277
  const attrValue = blockAttrs[2] || blockAttrs[3];
278
278
  const attribute = new shaka.hls.Attribute(attrName, attrValue);
279
279
  attributes.push(attribute);
280
+ parser.skipWhitespace();
280
281
  }
281
282
  }
282
283
 
@@ -86,7 +86,9 @@ shaka.media.ClosedCaptionParser = class {
86
86
  for (const captionPacket of captionPackets) {
87
87
  const uint8ArrayData =
88
88
  shaka.util.BufferUtils.toUint8(captionPacket.packet);
89
- this.ceaDecoder_.extract(uint8ArrayData, captionPacket.pts);
89
+ if (uint8ArrayData.length > 0) {
90
+ this.ceaDecoder_.extract(uint8ArrayData, captionPacket.pts);
91
+ }
90
92
  }
91
93
 
92
94
  // Decode and return the parsed captions.
@@ -75,6 +75,18 @@ shaka.media.ContentWorkarounds = class {
75
75
  newType: ContentWorkarounds.BOX_TYPE_ENCV_,
76
76
  });
77
77
  })
78
+ .fullBox('avc3', (box) => {
79
+ boxesToModify.push({
80
+ box,
81
+ newType: ContentWorkarounds.BOX_TYPE_ENCV_,
82
+ });
83
+ })
84
+ .fullBox('ac-3', (box) => {
85
+ boxesToModify.push({
86
+ box,
87
+ newType: ContentWorkarounds.BOX_TYPE_ENCA_,
88
+ });
89
+ })
78
90
  .fullBox('ec-3', (box) => {
79
91
  boxesToModify.push({
80
92
  box,
@@ -125,6 +125,9 @@ shaka.media.DrmEngine = class {
125
125
 
126
126
  /** @const {!shaka.util.Destroyer} */
127
127
  this.destroyer_ = new shaka.util.Destroyer(() => this.destroyNow_());
128
+
129
+ /** @private {boolean} */
130
+ this.srcEquals_ = false;
128
131
  }
129
132
 
130
133
  /** @override */
@@ -180,6 +183,7 @@ shaka.media.DrmEngine = class {
180
183
  this.config_ = null;
181
184
  this.onError_ = () => {};
182
185
  this.playerInterface_ = null;
186
+ this.srcEquals_ = false;
183
187
  }
184
188
 
185
189
  /**
@@ -193,6 +197,13 @@ shaka.media.DrmEngine = class {
193
197
  this.config_ = config;
194
198
  }
195
199
 
200
+ /**
201
+ * @param {!boolean} value
202
+ */
203
+ setSrcEquals(value) {
204
+ this.srcEquals_ = value;
205
+ }
206
+
196
207
  /**
197
208
  * Initialize the drm engine for storing and deleting stored content.
198
209
  *
@@ -351,7 +362,7 @@ shaka.media.DrmEngine = class {
351
362
  // We should get the decodingInfo results for the variants after we filling
352
363
  // in the drm infos, and before queryMediaKeys_().
353
364
  await shaka.util.StreamUtils.getDecodingInfosForVariants(variants,
354
- this.usePersistentLicenses_);
365
+ this.usePersistentLicenses_, this.srcEquals_);
355
366
 
356
367
  const hasDrmInfo = hadDrmInfo || Object.keys(this.config_.servers).length;
357
368
  // An unencrypted content is initialized.
@@ -539,6 +550,7 @@ shaka.media.DrmEngine = class {
539
550
  tasks.push(session.remove());
540
551
 
541
552
  await Promise.all(tasks);
553
+ this.activeSessions_.delete(session);
542
554
  }
543
555
 
544
556
  /**
@@ -1813,9 +1825,16 @@ shaka.media.DrmEngine = class {
1813
1825
  // audio adaptations, so we shouldn't have to worry about checking
1814
1826
  // robustness.
1815
1827
  if (drm1.keySystem == drm2.keySystem) {
1828
+ /** @type {Array<shaka.extern.InitDataOverride>} */
1816
1829
  let initData = [];
1817
1830
  initData = initData.concat(drm1.initData || []);
1818
1831
  initData = initData.concat(drm2.initData || []);
1832
+ initData = initData.filter((d, i) => {
1833
+ return d.keyId === undefined || i === initData.findIndex((d2) => {
1834
+ return d2.keyId === d.keyId;
1835
+ });
1836
+ });
1837
+
1819
1838
  const keyIds = drm1.keyIds && drm2.keyIds ?
1820
1839
  new Set([...drm1.keyIds, ...drm2.keyIds]) :
1821
1840
  drm1.keyIds || drm2.keyIds;
@@ -154,8 +154,10 @@ shaka.media.GapJumpingController = class {
154
154
  }
155
155
  // Don't gap jump while paused, so that you don't constantly jump ahead
156
156
  // while paused on a livestream. We make an exception for time 0, since we
157
- // may be _required_ to seek on startup before play can begin.
158
- if (this.video_.paused && this.video_.currentTime != 0) {
157
+ // may be _required_ to seek on startup before play can begin, but only if
158
+ // autoplay is enabled.
159
+ if (this.video_.paused && (this.video_.currentTime != 0 ||
160
+ (!this.video_.autoplay && this.video_.currentTime == 0))) {
159
161
  return;
160
162
  }
161
163
 
@@ -215,8 +217,10 @@ shaka.media.GapJumpingController = class {
215
217
  this.didFireLargeGap_ = true;
216
218
 
217
219
  // Event firing is synchronous.
218
- const event = new shaka.util.FakeEvent(
219
- 'largegap', {'currentTime': currentTime, 'gapSize': jumpSize});
220
+ const data = new Map()
221
+ .set('currentTime', currentTime)
222
+ .set('gapSize', jumpSize);
223
+ const event = new shaka.util.FakeEvent('largegap', data);
220
224
  event.cancelable = true;
221
225
  this.onEvent_(event);
222
226
 
@@ -48,8 +48,7 @@ shaka.media.MediaSourceEngine = class {
48
48
  * @param {!function(!Array.<shaka.extern.ID3Metadata>, number, ?number)=}
49
49
  * onMetadata
50
50
  */
51
- constructor(video, closedCaptionParser, textDisplayer,
52
- onMetadata) {
51
+ constructor(video, closedCaptionParser, textDisplayer, onMetadata) {
53
52
  /** @private {HTMLMediaElement} */
54
53
  this.video_ = video;
55
54
 
@@ -80,7 +80,7 @@ shaka.media.SrcEqualsPlayhead = class {
80
80
  // We listen for the loaded-data-event so that we know when we can
81
81
  // interact with |currentTime|.
82
82
  const onLoaded = () => {
83
- if (this.startTime_ == null) {
83
+ if (this.startTime_ == null || this.startTime_ == 0) {
84
84
  this.started_ = true;
85
85
  } else {
86
86
  // Startup is complete only when the video element acknowledges the