shaka-player 3.1.1 → 3.1.5

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 (151) hide show
  1. package/AUTHORS +7 -2
  2. package/CHANGELOG.md +199 -0
  3. package/CONTRIBUTORS +9 -3
  4. package/build/generateExterns.js +1 -1
  5. package/build/misspellings.txt +1 -0
  6. package/build/shakaBuildHelpers.py +6 -5
  7. package/build/subprocessWindowsPatch.py +103 -0
  8. package/demo/common/assets.js +1 -1
  9. package/dist/controls.css +2 -2
  10. package/dist/controls.css.map +1 -1
  11. package/dist/demo.compiled.debug.js +1 -1
  12. package/dist/demo.compiled.js +1 -1
  13. package/dist/deps.js +7 -7
  14. package/dist/locales.js +35 -33
  15. package/dist/receiver.compiled.debug.js +1 -1
  16. package/dist/receiver.compiled.js +1 -1
  17. package/dist/shaka-player.compiled.d.ts +171 -3
  18. package/dist/shaka-player.compiled.debug.d.ts +171 -3
  19. package/dist/shaka-player.compiled.debug.externs.js +179 -1
  20. package/dist/shaka-player.compiled.debug.js +365 -344
  21. package/dist/shaka-player.compiled.debug.map +3 -3
  22. package/dist/shaka-player.compiled.externs.js +179 -1
  23. package/dist/shaka-player.compiled.js +806 -798
  24. package/dist/shaka-player.compiled.map +3 -3
  25. package/dist/shaka-player.ui.d.ts +171 -3
  26. package/dist/shaka-player.ui.debug.d.ts +171 -3
  27. package/dist/shaka-player.ui.debug.externs.js +181 -3
  28. package/dist/shaka-player.ui.debug.js +458 -435
  29. package/dist/shaka-player.ui.debug.map +4 -4
  30. package/dist/shaka-player.ui.externs.js +181 -3
  31. package/dist/shaka-player.ui.js +927 -918
  32. package/dist/shaka-player.ui.map +4 -4
  33. package/docs/tutorials/fairplay.md +1 -1
  34. package/docs/tutorials/faq.md +14 -0
  35. package/docs/tutorials/index.json +2 -2
  36. package/docs/tutorials/upgrade-v3.md +38 -0
  37. package/docs/tutorials/upgrade.md +3 -3
  38. package/externs/ima.js +4 -0
  39. package/externs/shaka/ads.js +2 -0
  40. package/externs/shaka/player.js +6 -0
  41. package/externs/shaka/text.js +10 -0
  42. package/karma.conf.js +1 -0
  43. package/lib/ads/ad_manager.js +29 -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 +3 -2
  47. package/lib/cast/cast_receiver.js +2 -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 +100 -5
  51. package/lib/dash/segment_list.js +1 -1
  52. package/lib/dash/segment_template.js +7 -4
  53. package/lib/hls/hls_parser.js +44 -17
  54. package/lib/media/closed_caption_parser.js +3 -1
  55. package/lib/media/content_workarounds.js +12 -0
  56. package/lib/media/drm_engine.js +20 -1
  57. package/lib/media/gap_jumping_controller.js +8 -4
  58. package/lib/media/media_source_engine.js +1 -2
  59. package/lib/media/playhead.js +1 -1
  60. package/lib/media/presentation_timeline.js +3 -1
  61. package/lib/media/segment_reference.js +8 -0
  62. package/lib/media/streaming_engine.js +28 -19
  63. package/lib/net/networking_engine.js +2 -1
  64. package/lib/player.js +108 -54
  65. package/lib/polyfill/encryption_scheme.js +2 -0
  66. package/lib/polyfill/fullscreen.js +2 -0
  67. package/lib/polyfill/mathround.js +2 -0
  68. package/lib/polyfill/media_capabilities.js +76 -22
  69. package/lib/polyfill/mediasource.js +8 -0
  70. package/lib/polyfill/orientation.js +3 -1
  71. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  72. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  73. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  74. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  75. package/lib/polyfill/pip_webkit.js +2 -0
  76. package/lib/polyfill/storage_estimate.js +2 -0
  77. package/lib/polyfill/video_play_promise.js +2 -0
  78. package/lib/polyfill/videoplaybackquality.js +2 -0
  79. package/lib/polyfill/vttcue.js +2 -0
  80. package/lib/text/cue.js +17 -0
  81. package/lib/text/mp4_ttml_parser.js +3 -0
  82. package/lib/text/text_engine.js +14 -2
  83. package/lib/text/ttml_text_parser.js +135 -87
  84. package/lib/text/ui_text_displayer.js +233 -83
  85. package/lib/text/vtt_text_parser.js +4 -2
  86. package/lib/util/fake_event.js +53 -7
  87. package/lib/util/mp4_box_parsers.js +1 -1
  88. package/lib/util/periods.js +111 -35
  89. package/lib/util/platform.js +13 -1
  90. package/lib/util/stream_utils.js +79 -10
  91. package/lib/util/xml_utils.js +19 -0
  92. package/package.json +2 -2
  93. package/test/cast/cast_proxy_unit.js +12 -6
  94. package/test/cast/cast_receiver_integration.js +1 -1
  95. package/test/cast/cast_utils_unit.js +1 -1
  96. package/test/cea/mp4_cea_parser_unit.js +26 -0
  97. package/test/dash/dash_parser_live_unit.js +1 -0
  98. package/test/dash/dash_parser_manifest_unit.js +220 -2
  99. package/test/dash/dash_parser_segment_template_unit.js +3 -3
  100. package/test/hls/hls_parser_unit.js +5 -1
  101. package/test/media/closed_caption_parser_unit.js +20 -0
  102. package/test/media/drm_engine_unit.js +56 -0
  103. package/test/media/playhead_unit.js +27 -1
  104. package/test/media/presentation_timeline_unit.js +19 -0
  105. package/test/media/streaming_engine_integration.js +2 -1
  106. package/test/media/streaming_engine_unit.js +148 -3
  107. package/test/player_integration.js +38 -0
  108. package/test/player_unit.js +101 -0
  109. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  110. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  111. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  112. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  113. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  114. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  115. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  116. package/test/test/assets/3675/dash_0.mpd +30 -0
  117. package/test/test/assets/3675/dash_1.mpd +30 -0
  118. package/test/test/assets/3675/dash_2.mpd +30 -0
  119. package/test/test/assets/3675/dash_3.mpd +30 -0
  120. package/test/test/assets/3675/dash_4.mpd +30 -0
  121. package/test/test/assets/3675/dash_5.mpd +26 -0
  122. package/test/test/assets/3675/init-stream0.m4s +0 -0
  123. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  124. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  125. package/test/test/util/fake_ad_manager.js +4 -1
  126. package/test/test/util/simple_fakes.js +5 -3
  127. package/test/test/util/ttml_utils.js +81 -0
  128. package/test/text/mp4_ttml_parser_unit.js +11 -18
  129. package/test/text/ttml_text_parser_unit.js +542 -220
  130. package/test/text/ui_text_displayer_unit.js +95 -18
  131. package/test/text/vtt_text_parser_unit.js +17 -1
  132. package/test/ui/localization_unit.js +15 -14
  133. package/test/ui/text_displayer_layout_unit.js +52 -1
  134. package/test/util/mp4_box_parsers_unit.js +28 -0
  135. package/test/util/periods_unit.js +190 -2
  136. package/test/util/stream_utils_unit.js +59 -2
  137. package/ui/airplay_button.js +5 -0
  138. package/ui/big_play_button.js +10 -0
  139. package/ui/cast_button.js +2 -3
  140. package/ui/controls.js +8 -6
  141. package/ui/less/buttons.less +0 -4
  142. package/ui/less/containers.less +10 -5
  143. package/ui/locales/en.json +1 -0
  144. package/ui/locales/ru.json +6 -6
  145. package/ui/locales/source.json +4 -0
  146. package/ui/localization.js +28 -51
  147. package/ui/pip_button.js +2 -3
  148. package/ui/play_button.js +10 -10
  149. package/ui/small_play_button.js +12 -0
  150. package/ui/text_selection.js +1 -0
  151. package/docs/tutorials/upgrade-v2-4.md +0 -481
@@ -14,10 +14,12 @@ goog.provide('shaka.ads.ServerSideAdManager');
14
14
  goog.require('goog.asserts');
15
15
  goog.require('shaka.ads.ServerSideAd');
16
16
  goog.require('shaka.log');
17
+ goog.require('shaka.util.IReleasable');
17
18
 
18
19
 
19
20
  /**
20
21
  * A class responsible for server-side ad interactions.
22
+ * @implements {shaka.util.IReleasable}
21
23
  */
22
24
  shaka.ads.ServerSideAdManager = class {
23
25
  /**
@@ -75,9 +77,7 @@ shaka.ads.ServerSideAdManager = class {
75
77
 
76
78
  this.onEvent_(new shaka.util.FakeEvent(
77
79
  shaka.ads.AdManager.IMA_STREAM_MANAGER_LOADED,
78
- {
79
- 'imaStreamManager': this.streamManager_,
80
- }));
80
+ (new Map()).set('imaStreamManager', this.streamManager_)));
81
81
 
82
82
  // Events
83
83
  this.eventManager_.listen(this.streamManager_,
@@ -201,11 +201,27 @@ shaka.ads.ServerSideAdManager = class {
201
201
  * Resets the stream manager and removes any continuous polling.
202
202
  */
203
203
  stop() {
204
- this.streamManager_.reset();
204
+ // TODO:
205
+ // For SS DAI streams, if a different asset gets unloaded as
206
+ // part of the process
207
+ // of loading a DAI asset, stream manager state gets reset and we
208
+ // don't get any ad events.
209
+ // We need to figure out if it makes sense to stop the SS
210
+ // manager on unload, and, if it does, find
211
+ // a way to do it safely.
212
+ // this.streamManager_.reset();
205
213
  this.backupUrl_ = '';
206
214
  this.snapForwardTime_ = null;
207
215
  }
208
216
 
217
+ /** @override */
218
+ release() {
219
+ this.stop();
220
+ if (this.eventManager_) {
221
+ this.eventManager_.release();
222
+ }
223
+ }
224
+
209
225
  /**
210
226
  * @param {string} type
211
227
  * @param {Uint8Array|string} data
@@ -280,7 +296,7 @@ shaka.ads.ServerSideAdManager = class {
280
296
  }
281
297
 
282
298
  this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STARTED,
283
- {'ad': this.ad_}));
299
+ (new Map()).set('ad', this.ad_)));
284
300
  this.adContainer_.setAttribute('ad-active', 'true');
285
301
  }
286
302
 
@@ -306,9 +322,8 @@ shaka.ads.ServerSideAdManager = class {
306
322
  onLoaded_(e) {
307
323
  const now = Date.now() / 1000;
308
324
  const loadTime = now - this.streamRequestStartTime_;
309
- this.onEvent_(
310
- new shaka.util.FakeEvent(shaka.ads.AdManager.ADS_LOADED,
311
- {'loadTime': loadTime}));
325
+ this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.ADS_LOADED,
326
+ (new Map()).set('loadTime', loadTime)));
312
327
 
313
328
  const streamData = e.getStreamData();
314
329
  const url = streamData.url;
@@ -368,8 +383,8 @@ shaka.ads.ServerSideAdManager = class {
368
383
  cuePoints.push(shakaCuePoint);
369
384
  }
370
385
 
371
- this.onEvent_(
372
- new shaka.util.FakeEvent(shaka.ads.AdManager.CUEPOINTS_CHANGED,
373
- {'cuepoints': cuePoints}));
386
+ this.onEvent_(new shaka.util.FakeEvent(
387
+ shaka.ads.AdManager.CUEPOINTS_CHANGED,
388
+ (new Map()).set('cuepoints', cuePoints)));
374
389
  }
375
390
  };
@@ -525,7 +525,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
525
525
  goog.asserts.assert(error instanceof shaka.util.Error,
526
526
  'Wrong error type!');
527
527
  const eventType = shaka.Player.EventName.Error;
528
- const event = new shaka.util.FakeEvent(eventType, {'detail': error});
528
+ const data = (new Map()).set('detail', error);
529
+ const event = new shaka.util.FakeEvent(eventType, data);
529
530
  this.localPlayer_.dispatchEvent(event);
530
531
  });
531
532
  }
@@ -598,7 +599,7 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
598
599
 
599
600
  // Convert this real Event into a FakeEvent for dispatch from our
600
601
  // FakeEventListener.
601
- const fakeEvent = new shaka.util.FakeEvent(event.type, event);
602
+ const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
602
603
  this.videoEventTarget_.dispatchEvent(fakeEvent);
603
604
  }
604
605
 
@@ -407,7 +407,8 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
407
407
  goog.asserts.assert(error instanceof shaka.util.Error,
408
408
  'Wrong error type!');
409
409
  const eventType = shaka.Player.EventName.Error;
410
- const event = new shaka.util.FakeEvent(eventType, {'detail': error});
410
+ const data = (new Map()).set('detail', error);
411
+ const event = new shaka.util.FakeEvent(eventType, data);
411
412
  // Only dispatch the event if the player still exists.
412
413
  if (this.player_) {
413
414
  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.
@@ -533,6 +545,7 @@ shaka.dash.DashParser = class {
533
545
  for (const {i, item: elem, next} of enumerate(periodNodes)) {
534
546
  const start = /** @type {number} */ (
535
547
  XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
548
+ const periodId = elem.id;
536
549
  const givenDuration =
537
550
  XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
538
551
 
@@ -566,6 +579,42 @@ shaka.dash.DashParser = class {
566
579
  periodDuration = givenDuration;
567
580
  }
568
581
 
582
+ /**
583
+ * This is to improve robustness when the player observes manifest with
584
+ * past periods that are inconsistent to previous ones.
585
+ *
586
+ * This may happen when a CDN or proxy server switches its upstream from
587
+ * one encoder to another redundant encoder.
588
+ *
589
+ * Skip periods that match all of the following criteria:
590
+ * - Start time is earlier than latest period start time ever seen
591
+ * - Period ID is never seen in the previous manifest
592
+ * - Not the last period in the manifest
593
+ *
594
+ * Periods that meet the aforementioned criteria are considered invalid
595
+ * and should be safe to discard.
596
+ */
597
+
598
+ if (this.largestPeriodStartTime_ !== null &&
599
+ periodId !== null && start !== null &&
600
+ start < this.largestPeriodStartTime_ &&
601
+ !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
602
+ i + 1 != periodNodes.length) {
603
+ shaka.log.debug(
604
+ `Skipping Period with ID ${periodId} as its start time is smaller` +
605
+ ' than the largest period start time that has been seen, and ID ' +
606
+ 'is unseen before');
607
+ continue;
608
+ }
609
+
610
+
611
+ // Save maximum period start time if it is the last period
612
+ if (start !== null &&
613
+ (this.largestPeriodStartTime_ === null ||
614
+ start > this.largestPeriodStartTime_)) {
615
+ this.largestPeriodStartTime_ = start;
616
+ }
617
+
569
618
  // Parse child nodes.
570
619
  const info = {
571
620
  start: start,
@@ -597,6 +646,9 @@ shaka.dash.DashParser = class {
597
646
  prevEnd = start + periodDuration;
598
647
  } // end of period parsing loop
599
648
 
649
+ // Replace previous seen periods with the current one.
650
+ this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
651
+
600
652
  if (presentationDuration != null) {
601
653
  if (prevEnd != presentationDuration) {
602
654
  shaka.log.warning(
@@ -808,6 +860,25 @@ shaka.dash.DashParser = class {
808
860
  }
809
861
  }
810
862
 
863
+ // Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
864
+ let videoRange;
865
+ const videoRangeScheme = 'urn:mpeg:mpegB:cicp:TransferCharacteristics';
866
+ const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
867
+ switch (cicp) {
868
+ case 1:
869
+ case 6:
870
+ case 13:
871
+ case 14:
872
+ case 15:
873
+ return 'SDR';
874
+ case 16:
875
+ return 'PQ';
876
+ case 18:
877
+ return 'HLG';
878
+ }
879
+ return undefined;
880
+ };
881
+
811
882
  const essentialProperties =
812
883
  XmlUtils.findChildren(elem, 'EssentialProperty');
813
884
  // ID of real AdaptationSet if this is a trick mode set:
@@ -817,11 +888,26 @@ shaka.dash.DashParser = class {
817
888
  const schemeId = prop.getAttribute('schemeIdUri');
818
889
  if (schemeId == 'http://dashif.org/guidelines/trickmode') {
819
890
  trickModeFor = prop.getAttribute('value');
891
+ } else if (schemeId == videoRangeScheme) {
892
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
893
+ parseInt(prop.getAttribute('value'), 10),
894
+ );
820
895
  } else {
821
896
  unrecognizedEssentialProperty = true;
822
897
  }
823
898
  }
824
899
 
900
+ const supplementalProperties =
901
+ XmlUtils.findChildren(elem, 'SupplementalProperty');
902
+ for (const prop of supplementalProperties) {
903
+ const schemeId = prop.getAttribute('schemeIdUri');
904
+ if (schemeId == videoRangeScheme) {
905
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
906
+ parseInt(prop.getAttribute('value'), 10),
907
+ );
908
+ }
909
+ }
910
+
825
911
  const accessibilities = XmlUtils.findChildren(elem, 'Accessibility');
826
912
  const LanguageUtils = shaka.util.LanguageUtils;
827
913
  const closedCaptions = new Map();
@@ -858,7 +944,9 @@ shaka.dash.DashParser = class {
858
944
  channel = channelAndLanguage[0].startsWith('CC') ?
859
945
  channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
860
946
 
861
- language = channelAndLanguage[1]; // 3 letters (ISO 639-2)
947
+ // 3 letters (ISO 639-2). In b/187442669, we saw a blank string
948
+ // (CC2=;CC3=), so default to "und" (the code for "undetermined").
949
+ language = channelAndLanguage[1] || 'und';
862
950
  }
863
951
  closedCaptions.set(channel, LanguageUtils.normalize(language));
864
952
  }
@@ -945,8 +1033,13 @@ shaka.dash.DashParser = class {
945
1033
  // Parse Representations into Streams.
946
1034
  const representations = XmlUtils.findChildren(elem, 'Representation');
947
1035
  const streams = representations.map((representation) => {
948
- return this.parseRepresentation_(context, contentProtection, kind,
949
- language, label, main, roleValues, closedCaptions, representation);
1036
+ const parsedRepresentation = this.parseRepresentation_(context,
1037
+ contentProtection, kind, language, label, main, roleValues,
1038
+ closedCaptions, representation);
1039
+ if (parsedRepresentation) {
1040
+ parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange;
1041
+ }
1042
+ return parsedRepresentation;
950
1043
  }).filter((s) => !!s);
951
1044
 
952
1045
  if (streams.length == 0) {
@@ -1122,8 +1215,10 @@ shaka.dash.DashParser = class {
1122
1215
 
1123
1216
  let forced = false;
1124
1217
  if (isText) {
1125
- // See: https://github.com/google/shaka-player/issues/2122
1126
- forced = roles.includes('forced_subtitle');
1218
+ // See: https://github.com/google/shaka-player/issues/2122 and
1219
+ // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/165
1220
+ forced = roles.includes('forced_subtitle') ||
1221
+ roles.includes('forced-subtitle');
1127
1222
  }
1128
1223
 
1129
1224
  let tilesLayout;
@@ -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);
@@ -390,17 +390,17 @@ shaka.dash.SegmentTemplate = class {
390
390
 
391
391
  // Relative to the presentation.
392
392
  const segmentStart = segmentPeriodTime + periodStart;
393
+ const trueSegmentEnd = segmentStart + segmentDuration;
393
394
  // Cap the segment end at the period end so that references from the
394
395
  // next period will fit neatly after it.
395
- const segmentEnd = Math.min(segmentStart + segmentDuration,
396
- getPeriodEnd());
396
+ const segmentEnd = Math.min(trueSegmentEnd, getPeriodEnd());
397
397
 
398
398
  // This condition will be true unless the segmentStart was >= periodEnd.
399
399
  // If we've done the position calculations correctly, this won't happen.
400
400
  goog.asserts.assert(segmentStart < segmentEnd,
401
401
  'Generated a segment outside of the period!');
402
402
 
403
- return new shaka.media.SegmentReference(
403
+ const ref = new shaka.media.SegmentReference(
404
404
  segmentStart,
405
405
  segmentEnd,
406
406
  getUris,
@@ -410,6 +410,9 @@ shaka.dash.SegmentTemplate = class {
410
410
  timestampOffset,
411
411
  /* appendWindowStart= */ periodStart,
412
412
  /* appendWindowEnd= */ getPeriodEnd());
413
+ // This is necessary information for thumbnail streams:
414
+ ref.trueEndTime = trueSegmentEnd;
415
+ return ref;
413
416
  };
414
417
 
415
418
  for (let position = minPosition; position <= maxPosition; ++position) {
@@ -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;
@@ -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;