shaka-player 3.1.2 → 3.1.6

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 (145) 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 +170 -1
  5. package/CONTRIBUTORS +3 -0
  6. package/build/checkversion.py +1 -1
  7. package/build/generateExterns.js +1 -1
  8. package/build/misspellings.txt +1 -0
  9. package/build/shakaBuildHelpers.py +6 -5
  10. package/build/subprocessWindowsPatch.py +103 -0
  11. package/build/test.py +6 -0
  12. package/demo/common/assets.js +1 -1
  13. package/dist/controls.css +2 -2
  14. package/dist/controls.css.map +1 -1
  15. package/dist/demo.compiled.debug.js +1 -1
  16. package/dist/demo.compiled.js +1 -1
  17. package/dist/deps.js +7 -7
  18. package/dist/locales.js +35 -35
  19. package/dist/receiver.compiled.debug.js +1 -1
  20. package/dist/receiver.compiled.js +1 -1
  21. package/dist/shaka-player.compiled.d.ts +172 -3
  22. package/dist/shaka-player.compiled.debug.d.ts +172 -3
  23. package/dist/shaka-player.compiled.debug.externs.js +184 -1
  24. package/dist/shaka-player.compiled.debug.js +390 -370
  25. package/dist/shaka-player.compiled.debug.map +3 -3
  26. package/dist/shaka-player.compiled.externs.js +184 -1
  27. package/dist/shaka-player.compiled.js +809 -805
  28. package/dist/shaka-player.compiled.map +3 -3
  29. package/dist/shaka-player.ui.d.ts +174 -14
  30. package/dist/shaka-player.ui.debug.d.ts +174 -14
  31. package/dist/shaka-player.ui.debug.externs.js +200 -26
  32. package/dist/shaka-player.ui.debug.js +431 -412
  33. package/dist/shaka-player.ui.debug.map +4 -4
  34. package/dist/shaka-player.ui.externs.js +200 -26
  35. package/dist/shaka-player.ui.js +934 -929
  36. package/dist/shaka-player.ui.map +4 -4
  37. package/docs/tutorials/fairplay.md +1 -1
  38. package/docs/tutorials/faq.md +14 -0
  39. package/externs/ima.js +4 -0
  40. package/externs/shaka/ads.js +2 -0
  41. package/externs/shaka/text.js +10 -0
  42. package/karma.conf.js +20 -15
  43. package/lib/ads/ad_manager.js +30 -11
  44. package/lib/ads/client_side_ad_manager.js +76 -86
  45. package/lib/ads/server_side_ad_manager.js +26 -11
  46. package/lib/cast/cast_proxy.js +6 -2
  47. package/lib/cast/cast_receiver.js +5 -1
  48. package/lib/cast/cast_sender.js +2 -2
  49. package/lib/cea/sei_processor.js +24 -17
  50. package/lib/dash/dash_parser.js +131 -24
  51. package/lib/dash/segment_list.js +1 -1
  52. package/lib/dash/segment_template.js +1 -1
  53. package/lib/hls/hls_parser.js +44 -17
  54. package/lib/hls/manifest_text_parser.js +1 -0
  55. package/lib/media/closed_caption_parser.js +3 -1
  56. package/lib/media/content_workarounds.js +12 -0
  57. package/lib/media/drm_engine.js +20 -1
  58. package/lib/media/gap_jumping_controller.js +8 -4
  59. package/lib/media/media_source_engine.js +1 -2
  60. package/lib/media/playhead.js +1 -1
  61. package/lib/media/region_observer.js +39 -33
  62. package/lib/media/region_timeline.js +17 -19
  63. package/lib/media/streaming_engine.js +8 -4
  64. package/lib/net/networking_engine.js +6 -1
  65. package/lib/offline/storage.js +10 -11
  66. package/lib/player.js +66 -30
  67. package/lib/polyfill/encryption_scheme.js +2 -0
  68. package/lib/polyfill/fullscreen.js +2 -0
  69. package/lib/polyfill/mathround.js +2 -0
  70. package/lib/polyfill/media_capabilities.js +73 -21
  71. package/lib/polyfill/mediasource.js +8 -0
  72. package/lib/polyfill/orientation.js +3 -1
  73. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  74. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  75. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  76. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  77. package/lib/polyfill/pip_webkit.js +2 -0
  78. package/lib/polyfill/storage_estimate.js +2 -0
  79. package/lib/polyfill/video_play_promise.js +2 -0
  80. package/lib/polyfill/videoplaybackquality.js +2 -0
  81. package/lib/polyfill/vttcue.js +2 -0
  82. package/lib/text/cue.js +6 -0
  83. package/lib/text/mp4_ttml_parser.js +3 -0
  84. package/lib/text/text_engine.js +14 -2
  85. package/lib/text/ttml_text_parser.js +135 -87
  86. package/lib/text/ui_text_displayer.js +213 -80
  87. package/lib/text/vtt_text_parser.js +4 -2
  88. package/lib/util/fake_event.js +53 -7
  89. package/lib/util/fake_event_target.js +21 -1
  90. package/lib/util/mp4_box_parsers.js +1 -1
  91. package/lib/util/periods.js +4 -1
  92. package/lib/util/platform.js +13 -1
  93. package/lib/util/stream_utils.js +77 -10
  94. package/lib/util/xml_utils.js +19 -0
  95. package/package.json +3 -6
  96. package/test/cast/cast_proxy_unit.js +12 -6
  97. package/test/cast/cast_receiver_integration.js +1 -1
  98. package/test/cast/cast_utils_unit.js +1 -1
  99. package/test/cea/mp4_cea_parser_unit.js +26 -0
  100. package/test/dash/dash_parser_live_unit.js +43 -0
  101. package/test/dash/dash_parser_manifest_unit.js +222 -3
  102. package/test/hls/hls_parser_unit.js +79 -1
  103. package/test/media/closed_caption_parser_unit.js +20 -0
  104. package/test/media/drm_engine_unit.js +56 -0
  105. package/test/media/playhead_unit.js +27 -1
  106. package/test/media/region_observer_unit.js +40 -5
  107. package/test/media/region_timeline_unit.js +40 -22
  108. package/test/media/streaming_engine_integration.js +2 -1
  109. package/test/media/streaming_engine_unit.js +145 -0
  110. package/test/player_integration.js +38 -0
  111. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  112. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  113. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  114. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  115. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  116. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  117. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  118. package/test/test/assets/3675/dash_0.mpd +30 -0
  119. package/test/test/assets/3675/dash_1.mpd +30 -0
  120. package/test/test/assets/3675/dash_2.mpd +30 -0
  121. package/test/test/assets/3675/dash_3.mpd +30 -0
  122. package/test/test/assets/3675/dash_4.mpd +30 -0
  123. package/test/test/assets/3675/dash_5.mpd +26 -0
  124. package/test/test/assets/3675/init-stream0.m4s +0 -0
  125. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  126. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  127. package/test/test/util/fake_ad_manager.js +4 -1
  128. package/test/test/util/ttml_utils.js +81 -0
  129. package/test/test/util/util.js +10 -7
  130. package/test/text/mp4_ttml_parser_unit.js +11 -18
  131. package/test/text/ttml_text_parser_unit.js +542 -220
  132. package/test/text/ui_text_displayer_unit.js +93 -20
  133. package/test/text/vtt_text_parser_unit.js +17 -1
  134. package/test/ui/localization_unit.js +15 -14
  135. package/test/ui/text_displayer_layout_unit.js +21 -8
  136. package/test/util/mp4_box_parsers_unit.js +28 -0
  137. package/test/util/stream_utils_unit.js +59 -2
  138. package/ui/cast_button.js +2 -3
  139. package/ui/controls.js +11 -6
  140. package/ui/less/buttons.less +0 -4
  141. package/ui/less/containers.less +6 -7
  142. package/ui/locales/ru.json +6 -6
  143. package/ui/localization.js +40 -87
  144. package/ui/pip_button.js +2 -3
  145. package/ui/text_selection.js +1 -0
@@ -120,6 +120,9 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
120
120
  this.videoProxy_ = null;
121
121
  this.playerProxy_ = null;
122
122
 
123
+ // FakeEventTarget implements IReleasable
124
+ super.release();
125
+
123
126
  return Promise.all(waitFor);
124
127
  }
125
128
 
@@ -525,7 +528,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
525
528
  goog.asserts.assert(error instanceof shaka.util.Error,
526
529
  'Wrong error type!');
527
530
  const eventType = shaka.Player.EventName.Error;
528
- const event = new shaka.util.FakeEvent(eventType, {'detail': error});
531
+ const data = (new Map()).set('detail', error);
532
+ const event = new shaka.util.FakeEvent(eventType, data);
529
533
  this.localPlayer_.dispatchEvent(event);
530
534
  });
531
535
  }
@@ -598,7 +602,7 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
598
602
 
599
603
  // Convert this real Event into a FakeEvent for dispatch from our
600
604
  // FakeEventListener.
601
- const fakeEvent = new shaka.util.FakeEvent(event.type, event);
605
+ const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
602
606
  this.videoEventTarget_.dispatchEvent(fakeEvent);
603
607
  }
604
608
 
@@ -238,6 +238,9 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
238
238
  this.shakaBus_ = null;
239
239
  this.genericBus_ = null;
240
240
 
241
+ // FakeEventTarget implements IReleasable
242
+ super.release();
243
+
241
244
  await Promise.all(waitFor);
242
245
 
243
246
  const manager = cast.receiver.CastReceiverManager.getInstance();
@@ -407,7 +410,8 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
407
410
  goog.asserts.assert(error instanceof shaka.util.Error,
408
411
  'Wrong error type!');
409
412
  const eventType = shaka.Player.EventName.Error;
410
- const event = new shaka.util.FakeEvent(eventType, {'detail': error});
413
+ const data = (new Map()).set('detail', error);
414
+ const event = new shaka.util.FakeEvent(eventType, data);
411
415
  // Only dispatch the event if the player still exists.
412
416
  if (this.player_) {
413
417
  this.player_.dispatchEvent(event);
@@ -647,7 +647,7 @@ shaka.cast.CastSender = class {
647
647
  case 'event': {
648
648
  const targetName = message['targetName'];
649
649
  const event = message['event'];
650
- const fakeEvent = new shaka.util.FakeEvent(event['type'], event);
650
+ const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
651
651
  this.onRemoteEvent_(targetName, fakeEvent);
652
652
  break;
653
653
  }
@@ -725,7 +725,7 @@ shaka.cast.CastSender = class {
725
725
 
726
726
  // Dispatch it through the Player proxy
727
727
  const fakeEvent = new shaka.util.FakeEvent(
728
- 'error', {'detail': shakaError});
728
+ 'error', (new Map()).set('detail', shakaError));
729
729
  this.onRemoteEvent_('player', fakeEvent);
730
730
 
731
731
  // Force this session to disconnect and transfer playback to the local
@@ -17,31 +17,31 @@ shaka.cea.SeiProcessor = class {
17
17
  * @return {!Iterable.<!Uint8Array>}
18
18
  */
19
19
  * process(naluData) {
20
- const emuCount = this.removeEmu_(naluData);
20
+ const naluClone = this.removeEmu(naluData);
21
21
 
22
22
  // The following is an implementation of section 7.3.2.3.1
23
23
  // in Rec. ITU-T H.264 (06/2019), the H.264 spec.
24
24
  let offset = 0;
25
25
 
26
- while (offset + emuCount < naluData.length) {
26
+ while (offset < naluClone.length) {
27
27
  let payloadType = 0; // SEI payload type as defined by H.264 spec
28
- while (naluData[offset] == 0xFF) {
28
+ while (naluClone[offset] == 0xFF) {
29
29
  payloadType += 255;
30
30
  offset++;
31
31
  }
32
- payloadType += naluData[offset++];
32
+ payloadType += naluClone[offset++];
33
33
 
34
34
  let payloadSize = 0; // SEI payload size as defined by H.264 spec
35
- while (naluData[offset] == 0xFF) {
35
+ while (naluClone[offset] == 0xFF) {
36
36
  payloadSize += 255;
37
37
  offset++;
38
38
  }
39
- payloadSize += naluData[offset++];
39
+ payloadSize += naluClone[offset++];
40
40
 
41
41
  // Payload type 4 is user_data_registered_itu_t_t35, as per the H.264
42
42
  // spec. This payload type contains caption data.
43
43
  if (payloadType == 0x04) {
44
- yield naluData.subarray(offset, offset + payloadSize);
44
+ yield naluClone.subarray(offset, offset + payloadSize);
45
45
  }
46
46
  offset += payloadSize;
47
47
  }
@@ -50,28 +50,35 @@ shaka.cea.SeiProcessor = class {
50
50
 
51
51
  /**
52
52
  * Removes H.264 emulation prevention bytes from the byte array.
53
+ *
54
+ * Note: Remove bytes by shifting will cause Chromium (VDA) to complain
55
+ * about conformance. Recreating a new array solves it.
56
+ *
53
57
  * @param {!Uint8Array} naluData NALU from which EMUs should be removed.
54
- * @return {number} The number of removed emulation prevention bytes.
55
- * @private
58
+ * @return {!Uint8Array} The NALU with the emulation prevention byte removed.
56
59
  */
57
- removeEmu_(naluData) {
60
+ removeEmu(naluData) {
61
+ let naluClone = naluData;
58
62
  let zeroCount = 0;
59
63
  let src = 0;
60
- let dst = 0;
61
- while (src < naluData.length) {
62
- if (zeroCount == 2 && naluData[src] == 0x03) {
64
+ while (src < naluClone.length) {
65
+ if (zeroCount == 2 && naluClone[src] == 0x03) {
66
+ // 0x00, 0x00, 0x03 pattern detected
63
67
  zeroCount = 0;
68
+
69
+ // Splice the array and recreate a new one, instead of shifting bytes
70
+ const newArr = [...naluClone];
71
+ newArr.splice(src, 1);
72
+ naluClone = new Uint8Array(newArr);
64
73
  } else {
65
- if (naluData[src] == 0x00) {
74
+ if (naluClone[src] == 0x00) {
66
75
  zeroCount++;
67
76
  } else {
68
77
  zeroCount = 0;
69
78
  }
70
- naluData[dst] = naluData[src];
71
- dst++;
72
79
  }
73
80
  src++;
74
81
  }
75
- return (src - dst);
82
+ return naluClone;
76
83
  }
77
84
  };
@@ -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.
@@ -384,6 +396,24 @@ shaka.dash.DashParser = class {
384
396
  this.config_.dash.autoCorrectDrift);
385
397
  }
386
398
 
399
+ presentationTimeline.setStatic(mpdType == 'static');
400
+
401
+ const isLive = presentationTimeline.isLive();
402
+
403
+ // If it's live, we check for an override.
404
+ if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
405
+ segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
406
+ }
407
+
408
+ // If it's null, that means segments are always available. This is always
409
+ // the case for VOD, and sometimes the case for live.
410
+ if (segmentAvailabilityDuration == null) {
411
+ segmentAvailabilityDuration = Infinity;
412
+ }
413
+
414
+ presentationTimeline.setSegmentAvailabilityDuration(
415
+ segmentAvailabilityDuration);
416
+
387
417
  const profiles = mpd.getAttribute('profiles') || '';
388
418
 
389
419
  /** @type {shaka.dash.DashParser.Context} */
@@ -406,7 +436,6 @@ shaka.dash.DashParser = class {
406
436
  const duration = periodsAndDuration.duration;
407
437
  const periods = periodsAndDuration.periods;
408
438
 
409
- presentationTimeline.setStatic(mpdType == 'static');
410
439
  if (mpdType == 'static' ||
411
440
  !periodsAndDuration.durationDerivedFromPeriods) {
412
441
  // Ignore duration calculated from Period lengths if this is dynamic.
@@ -425,6 +454,7 @@ shaka.dash.DashParser = class {
425
454
  this.lowLatencyMode_ = this.playerInterface_.isLowLatencyMode();
426
455
  }
427
456
  }
457
+
428
458
  if (this.lowLatencyMode_) {
429
459
  presentationTimeline.setAvailabilityTimeOffset(
430
460
  this.minTotalAvailabilityTimeOffset_);
@@ -438,22 +468,6 @@ shaka.dash.DashParser = class {
438
468
  'https://bit.ly/3clctcj for details.');
439
469
  }
440
470
 
441
- const isLive = presentationTimeline.isLive();
442
-
443
- // If it's live, we check for an override.
444
- if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
445
- segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
446
- }
447
-
448
- // If it's null, that means segments are always available. This is always
449
- // the case for VOD, and sometimes the case for live.
450
- if (segmentAvailabilityDuration == null) {
451
- segmentAvailabilityDuration = Infinity;
452
- }
453
-
454
- presentationTimeline.setSegmentAvailabilityDuration(
455
- segmentAvailabilityDuration);
456
-
457
471
  // Use @maxSegmentDuration to override smaller, derived values.
458
472
  presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
459
473
  if (goog.DEBUG) {
@@ -533,6 +547,7 @@ shaka.dash.DashParser = class {
533
547
  for (const {i, item: elem, next} of enumerate(periodNodes)) {
534
548
  const start = /** @type {number} */ (
535
549
  XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
550
+ const periodId = elem.id;
536
551
  const givenDuration =
537
552
  XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
538
553
 
@@ -566,6 +581,42 @@ shaka.dash.DashParser = class {
566
581
  periodDuration = givenDuration;
567
582
  }
568
583
 
584
+ /**
585
+ * This is to improve robustness when the player observes manifest with
586
+ * past periods that are inconsistent to previous ones.
587
+ *
588
+ * This may happen when a CDN or proxy server switches its upstream from
589
+ * one encoder to another redundant encoder.
590
+ *
591
+ * Skip periods that match all of the following criteria:
592
+ * - Start time is earlier than latest period start time ever seen
593
+ * - Period ID is never seen in the previous manifest
594
+ * - Not the last period in the manifest
595
+ *
596
+ * Periods that meet the aforementioned criteria are considered invalid
597
+ * and should be safe to discard.
598
+ */
599
+
600
+ if (this.largestPeriodStartTime_ !== null &&
601
+ periodId !== null && start !== null &&
602
+ start < this.largestPeriodStartTime_ &&
603
+ !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
604
+ i + 1 != periodNodes.length) {
605
+ shaka.log.debug(
606
+ `Skipping Period with ID ${periodId} as its start time is smaller` +
607
+ ' than the largest period start time that has been seen, and ID ' +
608
+ 'is unseen before');
609
+ continue;
610
+ }
611
+
612
+
613
+ // Save maximum period start time if it is the last period
614
+ if (start !== null &&
615
+ (this.largestPeriodStartTime_ === null ||
616
+ start > this.largestPeriodStartTime_)) {
617
+ this.largestPeriodStartTime_ = start;
618
+ }
619
+
569
620
  // Parse child nodes.
570
621
  const info = {
571
622
  start: start,
@@ -597,6 +648,9 @@ shaka.dash.DashParser = class {
597
648
  prevEnd = start + periodDuration;
598
649
  } // end of period parsing loop
599
650
 
651
+ // Replace previous seen periods with the current one.
652
+ this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
653
+
600
654
  if (presentationDuration != null) {
601
655
  if (prevEnd != presentationDuration) {
602
656
  shaka.log.warning(
@@ -648,8 +702,12 @@ shaka.dash.DashParser = class {
648
702
 
649
703
  const eventStreamNodes =
650
704
  XmlUtils.findChildren(periodInfo.node, 'EventStream');
705
+ const availabilityStart =
706
+ context.presentationTimeline.getSegmentAvailabilityStart();
707
+
651
708
  for (const node of eventStreamNodes) {
652
- this.parseEventStream_(periodInfo.start, periodInfo.duration, node);
709
+ this.parseEventStream_(
710
+ periodInfo.start, periodInfo.duration, node, availabilityStart);
653
711
  }
654
712
 
655
713
  const adaptationSetNodes =
@@ -808,6 +866,25 @@ shaka.dash.DashParser = class {
808
866
  }
809
867
  }
810
868
 
869
+ // Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
870
+ let videoRange;
871
+ const videoRangeScheme = 'urn:mpeg:mpegB:cicp:TransferCharacteristics';
872
+ const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
873
+ switch (cicp) {
874
+ case 1:
875
+ case 6:
876
+ case 13:
877
+ case 14:
878
+ case 15:
879
+ return 'SDR';
880
+ case 16:
881
+ return 'PQ';
882
+ case 18:
883
+ return 'HLG';
884
+ }
885
+ return undefined;
886
+ };
887
+
811
888
  const essentialProperties =
812
889
  XmlUtils.findChildren(elem, 'EssentialProperty');
813
890
  // ID of real AdaptationSet if this is a trick mode set:
@@ -817,11 +894,26 @@ shaka.dash.DashParser = class {
817
894
  const schemeId = prop.getAttribute('schemeIdUri');
818
895
  if (schemeId == 'http://dashif.org/guidelines/trickmode') {
819
896
  trickModeFor = prop.getAttribute('value');
897
+ } else if (schemeId == videoRangeScheme) {
898
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
899
+ parseInt(prop.getAttribute('value'), 10),
900
+ );
820
901
  } else {
821
902
  unrecognizedEssentialProperty = true;
822
903
  }
823
904
  }
824
905
 
906
+ const supplementalProperties =
907
+ XmlUtils.findChildren(elem, 'SupplementalProperty');
908
+ for (const prop of supplementalProperties) {
909
+ const schemeId = prop.getAttribute('schemeIdUri');
910
+ if (schemeId == videoRangeScheme) {
911
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
912
+ parseInt(prop.getAttribute('value'), 10),
913
+ );
914
+ }
915
+ }
916
+
825
917
  const accessibilities = XmlUtils.findChildren(elem, 'Accessibility');
826
918
  const LanguageUtils = shaka.util.LanguageUtils;
827
919
  const closedCaptions = new Map();
@@ -858,7 +950,9 @@ shaka.dash.DashParser = class {
858
950
  channel = channelAndLanguage[0].startsWith('CC') ?
859
951
  channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
860
952
 
861
- language = channelAndLanguage[1]; // 3 letters (ISO 639-2)
953
+ // 3 letters (ISO 639-2). In b/187442669, we saw a blank string
954
+ // (CC2=;CC3=), so default to "und" (the code for "undetermined").
955
+ language = channelAndLanguage[1] || 'und';
862
956
  }
863
957
  closedCaptions.set(channel, LanguageUtils.normalize(language));
864
958
  }
@@ -945,8 +1039,13 @@ shaka.dash.DashParser = class {
945
1039
  // Parse Representations into Streams.
946
1040
  const representations = XmlUtils.findChildren(elem, 'Representation');
947
1041
  const streams = representations.map((representation) => {
948
- return this.parseRepresentation_(context, contentProtection, kind,
949
- language, label, main, roleValues, closedCaptions, representation);
1042
+ const parsedRepresentation = this.parseRepresentation_(context,
1043
+ contentProtection, kind, language, label, main, roleValues,
1044
+ closedCaptions, representation);
1045
+ if (parsedRepresentation) {
1046
+ parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange;
1047
+ }
1048
+ return parsedRepresentation;
950
1049
  }).filter((s) => !!s);
951
1050
 
952
1051
  if (streams.length == 0) {
@@ -1122,8 +1221,10 @@ shaka.dash.DashParser = class {
1122
1221
 
1123
1222
  let forced = false;
1124
1223
  if (isText) {
1125
- // See: https://github.com/google/shaka-player/issues/2122
1126
- forced = roles.includes('forced_subtitle');
1224
+ // See: https://github.com/google/shaka-player/issues/2122 and
1225
+ // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/165
1226
+ forced = roles.includes('forced_subtitle') ||
1227
+ roles.includes('forced-subtitle');
1127
1228
  }
1128
1229
 
1129
1230
  let tilesLayout;
@@ -1637,9 +1738,10 @@ shaka.dash.DashParser = class {
1637
1738
  * @param {number} periodStart
1638
1739
  * @param {?number} periodDuration
1639
1740
  * @param {!Element} elem
1741
+ * @param {number} availabilityStart
1640
1742
  * @private
1641
1743
  */
1642
- parseEventStream_(periodStart, periodDuration, elem) {
1744
+ parseEventStream_(periodStart, periodDuration, elem, availabilityStart) {
1643
1745
  const XmlUtils = shaka.util.XmlUtils;
1644
1746
  const parseNumber = XmlUtils.parseNonNegativeInt;
1645
1747
 
@@ -1662,6 +1764,11 @@ shaka.dash.DashParser = class {
1662
1764
  endTime = Math.min(endTime, periodStart + periodDuration);
1663
1765
  }
1664
1766
 
1767
+ // Don't add unavailable regions to the timeline.
1768
+ if (endTime < availabilityStart) {
1769
+ continue;
1770
+ }
1771
+
1665
1772
  /** @type {shaka.extern.TimelineRegionInfo} */
1666
1773
  const region = {
1667
1774
  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);
@@ -414,18 +414,16 @@ shaka.hls.HlsParser = class {
414
414
  const uri = tag.getAttributeValue('URI');
415
415
  const language = tag.getAttributeValue('LANGUAGE');
416
416
  const value = tag.getAttributeValue('VALUE');
417
- const data = {
418
- 'id': id,
419
- };
417
+ const data = (new Map()).set('id', id);
420
418
  if (uri) {
421
- data.uri =
422
- shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri);
419
+ data.set('uri',
420
+ shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri));
423
421
  }
424
422
  if (language) {
425
- data.language = language;
423
+ data.set('language', language);
426
424
  }
427
425
  if (value) {
428
- data.value = value;
426
+ data.set('value', value);
429
427
  }
430
428
  const event = new shaka.util.FakeEvent('sessiondata', data);
431
429
  if (this.playerInterface_) {
@@ -529,6 +527,30 @@ shaka.hls.HlsParser = class {
529
527
  }
530
528
  }
531
529
 
530
+ // Now that the content has been fit, notify segments.
531
+ this.segmentsToNotifyByStream_ = [];
532
+ const streamsToNotify = [];
533
+ for (const variant of variants) {
534
+ for (const stream of [variant.video, variant.audio]) {
535
+ if (stream) {
536
+ streamsToNotify.push(stream);
537
+ }
538
+ }
539
+ }
540
+ await Promise.all(streamsToNotify.map(async (stream) => {
541
+ await stream.createSegmentIndex();
542
+ }));
543
+ for (const stream of streamsToNotify) {
544
+ this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
545
+ }
546
+ this.notifySegments_();
547
+
548
+ // This asserts that the live edge is being calculated from segment times.
549
+ // For VOD and event streams, this check should still pass.
550
+ goog.asserts.assert(
551
+ !this.presentationTimeline_.usingPresentationStartTime(),
552
+ 'We should not be using the presentation start time in HLS!');
553
+
532
554
  this.manifest_ = {
533
555
  presentationTimeline: this.presentationTimeline_,
534
556
  variants,
@@ -1151,7 +1173,7 @@ shaka.hls.HlsParser = class {
1151
1173
  // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
1152
1174
  // only be selected explicitly by the user, and we don't have a way to
1153
1175
  // represent that in our model.
1154
- const defaultAttrValue = tag.getAttribute('DEFAULT');
1176
+ const defaultAttrValue = tag.getAttributeValue('DEFAULT');
1155
1177
  const primary = defaultAttrValue == 'YES';
1156
1178
 
1157
1179
  const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
@@ -1538,12 +1560,6 @@ shaka.hls.HlsParser = class {
1538
1560
  }
1539
1561
 
1540
1562
  this.notifySegments_();
1541
-
1542
- // This asserts that the live edge is being calculated from segment times.
1543
- // For VOD and event streams, this check should still pass.
1544
- goog.asserts.assert(
1545
- !this.presentationTimeline_.usingPresentationStartTime(),
1546
- 'We should not be using the presentation start time in HLS!');
1547
1563
  }
1548
1564
 
1549
1565
  /**
@@ -1915,9 +1931,6 @@ shaka.hls.HlsParser = class {
1915
1931
  }
1916
1932
  }
1917
1933
 
1918
- this.segmentsToNotifyByStream_.push(references);
1919
- this.notifySegments_();
1920
-
1921
1934
  return references;
1922
1935
  }
1923
1936
 
@@ -2253,6 +2266,12 @@ shaka.hls.HlsParser = class {
2253
2266
  reader.rewind(1);
2254
2267
  };
2255
2268
 
2269
+ // We will look a few packet-lengths forward to find the first sync byte.
2270
+ // Note that we are using this method on what is already a subset of the
2271
+ // file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so
2272
+ // we can't look too far ahead to begin with.
2273
+ let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188);
2274
+
2256
2275
  // TODO: refactor this while loop for better readability.
2257
2276
  // eslint-disable-next-line no-constant-condition
2258
2277
  while (true) {
@@ -2261,8 +2280,16 @@ shaka.hls.HlsParser = class {
2261
2280
 
2262
2281
  syncByte = reader.readUint8();
2263
2282
  if (syncByte != 0x47) {
2283
+ if (syncByteScanLength > 0) {
2284
+ // This file could have started with a cut-off TS packet. Scan forward
2285
+ // until we find a sync byte.
2286
+ syncByteScanLength -= 1;
2287
+ continue;
2288
+ }
2264
2289
  fail();
2265
2290
  }
2291
+ // If we've found a sync byte, stop scanning forward for future packets.
2292
+ syncByteScanLength = 0;
2266
2293
 
2267
2294
  const flagsAndPacketId = reader.readUint16();
2268
2295
  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
 
@@ -87,7 +87,9 @@ shaka.media.ClosedCaptionParser = class {
87
87
  for (const captionPacket of captionPackets) {
88
88
  const uint8ArrayData =
89
89
  shaka.util.BufferUtils.toUint8(captionPacket.packet);
90
- this.ceaDecoder_.extract(uint8ArrayData, captionPacket.pts);
90
+ if (uint8ArrayData.length > 0) {
91
+ this.ceaDecoder_.extract(uint8ArrayData, captionPacket.pts);
92
+ }
91
93
  }
92
94
 
93
95
  // 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,
@@ -126,6 +126,9 @@ shaka.media.DrmEngine = class {
126
126
 
127
127
  /** @const {!shaka.util.Destroyer} */
128
128
  this.destroyer_ = new shaka.util.Destroyer(() => this.destroyNow_());
129
+
130
+ /** @private {boolean} */
131
+ this.srcEquals_ = false;
129
132
  }
130
133
 
131
134
  /** @override */
@@ -181,6 +184,7 @@ shaka.media.DrmEngine = class {
181
184
  this.config_ = null;
182
185
  this.onError_ = () => {};
183
186
  this.playerInterface_ = null;
187
+ this.srcEquals_ = false;
184
188
  }
185
189
 
186
190
  /**
@@ -194,6 +198,13 @@ shaka.media.DrmEngine = class {
194
198
  this.config_ = config;
195
199
  }
196
200
 
201
+ /**
202
+ * @param {!boolean} value
203
+ */
204
+ setSrcEquals(value) {
205
+ this.srcEquals_ = value;
206
+ }
207
+
197
208
  /**
198
209
  * Initialize the drm engine for storing and deleting stored content.
199
210
  *
@@ -356,7 +367,7 @@ shaka.media.DrmEngine = class {
356
367
  // in the drm infos, and before queryMediaKeys_().
357
368
  if (useMediaCapabilities) {
358
369
  await shaka.util.StreamUtils.getDecodingInfosForVariants(variants,
359
- this.usePersistentLicenses_);
370
+ this.usePersistentLicenses_, this.srcEquals_);
360
371
  } else {
361
372
  configsByKeySystem =
362
373
  this.prepareMediaKeyConfigsForVariants_(variants);
@@ -512,6 +523,7 @@ shaka.media.DrmEngine = class {
512
523
  tasks.push(session.remove());
513
524
 
514
525
  await Promise.all(tasks);
526
+ this.activeSessions_.delete(session);
515
527
  }
516
528
 
517
529
  /**
@@ -1877,9 +1889,16 @@ shaka.media.DrmEngine = class {
1877
1889
  // audio adaptations, so we shouldn't have to worry about checking
1878
1890
  // robustness.
1879
1891
  if (drm1.keySystem == drm2.keySystem) {
1892
+ /** @type {Array<shaka.extern.InitDataOverride>} */
1880
1893
  let initData = [];
1881
1894
  initData = initData.concat(drm1.initData || []);
1882
1895
  initData = initData.concat(drm2.initData || []);
1896
+ initData = initData.filter((d, i) => {
1897
+ return d.keyId === undefined || i === initData.findIndex((d2) => {
1898
+ return d2.keyId === d.keyId;
1899
+ });
1900
+ });
1901
+
1883
1902
  const keyIds = drm1.keyIds && drm2.keyIds ?
1884
1903
  new Set([...drm1.keyIds, ...drm2.keyIds]) :
1885
1904
  drm1.keyIds || drm2.keyIds;