shaka-player 3.1.0 → 3.1.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 (181) hide show
  1. package/AUTHORS +8 -2
  2. package/CHANGELOG.md +209 -0
  3. package/CONTRIBUTORS +10 -3
  4. package/README.md +3 -0
  5. package/build/apps.py +2 -2
  6. package/build/check.py +2 -2
  7. package/build/conformance.textproto +0 -9
  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/demo/common/asset.js +11 -0
  13. package/demo/common/assets.js +1 -1
  14. package/demo/common/message_ids.js +5 -0
  15. package/demo/config.js +2 -1
  16. package/demo/custom.js +84 -3
  17. package/demo/demo.less +13 -1
  18. package/demo/demo_utils.js +1 -0
  19. package/demo/input.js +31 -12
  20. package/demo/input_container.js +24 -1
  21. package/demo/locales/en.json +6 -1
  22. package/demo/locales/source.json +20 -0
  23. package/demo/main.js +5 -1
  24. package/dist/controls.css +2 -2
  25. package/dist/controls.css.map +1 -1
  26. package/dist/demo.compiled.debug.js +99 -91
  27. package/dist/demo.compiled.debug.map +3 -3
  28. package/dist/demo.compiled.js +99 -91
  29. package/dist/demo.compiled.map +3 -3
  30. package/dist/demo.css +1 -1
  31. package/dist/demo.css.map +1 -1
  32. package/dist/deps.js +9 -9
  33. package/dist/locales.js +36 -34
  34. package/dist/receiver.compiled.debug.js +74 -70
  35. package/dist/receiver.compiled.debug.map +3 -3
  36. package/dist/receiver.compiled.js +74 -70
  37. package/dist/receiver.compiled.map +3 -3
  38. package/dist/shaka-player.compiled.d.ts +165 -9
  39. package/dist/shaka-player.compiled.debug.d.ts +165 -9
  40. package/dist/shaka-player.compiled.debug.externs.js +179 -11
  41. package/dist/shaka-player.compiled.debug.js +367 -339
  42. package/dist/shaka-player.compiled.debug.map +4 -4
  43. package/dist/shaka-player.compiled.externs.js +179 -11
  44. package/dist/shaka-player.compiled.js +818 -807
  45. package/dist/shaka-player.compiled.map +4 -4
  46. package/dist/shaka-player.ui.d.ts +165 -9
  47. package/dist/shaka-player.ui.debug.d.ts +165 -9
  48. package/dist/shaka-player.ui.debug.externs.js +181 -13
  49. package/dist/shaka-player.ui.debug.js +459 -429
  50. package/dist/shaka-player.ui.debug.map +4 -4
  51. package/dist/shaka-player.ui.externs.js +181 -13
  52. package/dist/shaka-player.ui.js +973 -961
  53. package/dist/shaka-player.ui.map +4 -4
  54. package/docs/tutorials/fairplay.md +1 -1
  55. package/docs/tutorials/faq.md +25 -0
  56. package/docs/tutorials/index.json +2 -2
  57. package/docs/tutorials/upgrade-v3.md +38 -0
  58. package/docs/tutorials/upgrade.md +3 -3
  59. package/externs/ima.js +4 -0
  60. package/externs/media_capabilities.js +16 -0
  61. package/externs/shaka/ads.js +2 -0
  62. package/externs/shaka/net.js +2 -2
  63. package/externs/shaka/player.js +6 -0
  64. package/karma.conf.js +1 -0
  65. package/lib/abr/simple_abr_manager.js +1 -1
  66. package/lib/ads/ad_manager.js +29 -11
  67. package/lib/ads/client_side_ad_manager.js +76 -86
  68. package/lib/ads/server_side_ad_manager.js +26 -11
  69. package/lib/cast/cast_proxy.js +3 -2
  70. package/lib/cast/cast_receiver.js +2 -1
  71. package/lib/cast/cast_sender.js +2 -2
  72. package/lib/cea/sei_processor.js +24 -17
  73. package/lib/dash/dash_parser.js +107 -4
  74. package/lib/dash/segment_list.js +1 -1
  75. package/lib/dash/segment_template.js +45 -15
  76. package/lib/dependencies/all.js +15 -15
  77. package/lib/hls/hls_parser.js +46 -18
  78. package/lib/media/closed_caption_parser.js +3 -1
  79. package/lib/media/content_workarounds.js +18 -0
  80. package/lib/media/drm_engine.js +20 -1
  81. package/lib/media/gap_jumping_controller.js +8 -4
  82. package/lib/media/media_source_engine.js +1 -2
  83. package/lib/media/playhead.js +1 -1
  84. package/lib/media/presentation_timeline.js +3 -1
  85. package/lib/media/segment_index.js +38 -12
  86. package/lib/media/segment_reference.js +8 -0
  87. package/lib/media/streaming_engine.js +30 -21
  88. package/lib/net/http_fetch_plugin.js +2 -2
  89. package/lib/net/networking_engine.js +3 -2
  90. package/lib/offline/storage.js +4 -0
  91. package/lib/player.js +113 -56
  92. package/lib/polyfill/encryption_scheme.js +2 -0
  93. package/lib/polyfill/fullscreen.js +2 -0
  94. package/lib/polyfill/mathround.js +2 -0
  95. package/lib/polyfill/media_capabilities.js +62 -23
  96. package/lib/polyfill/mediasource.js +8 -0
  97. package/lib/polyfill/orientation.js +3 -1
  98. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  99. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  100. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  101. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  102. package/lib/polyfill/pip_webkit.js +2 -0
  103. package/lib/polyfill/storage_estimate.js +2 -0
  104. package/lib/polyfill/video_play_promise.js +2 -0
  105. package/lib/polyfill/videoplaybackquality.js +2 -0
  106. package/lib/polyfill/vttcue.js +2 -0
  107. package/lib/text/cue.js +11 -0
  108. package/lib/text/mp4_ttml_parser.js +3 -0
  109. package/lib/text/text_engine.js +14 -2
  110. package/lib/text/ttml_text_parser.js +52 -30
  111. package/lib/text/ui_text_displayer.js +255 -82
  112. package/lib/text/vtt_text_parser.js +4 -2
  113. package/lib/util/error.js +1 -4
  114. package/lib/util/fake_event.js +53 -7
  115. package/lib/util/mime_utils.js +33 -0
  116. package/lib/util/mp4_box_parsers.js +1 -1
  117. package/lib/util/mp4_parser.js +14 -0
  118. package/lib/util/networking.js +1 -1
  119. package/lib/util/periods.js +132 -37
  120. package/lib/util/platform.js +13 -1
  121. package/lib/util/stream_utils.js +46 -7
  122. package/package.json +3 -3
  123. package/test/cast/cast_proxy_unit.js +12 -6
  124. package/test/cast/cast_receiver_integration.js +1 -1
  125. package/test/cast/cast_utils_unit.js +1 -1
  126. package/test/cea/mp4_cea_parser_unit.js +26 -0
  127. package/test/dash/dash_parser_live_unit.js +1 -0
  128. package/test/dash/dash_parser_manifest_unit.js +187 -1
  129. package/test/dash/dash_parser_segment_template_unit.js +3 -3
  130. package/test/demo/demo_unit.js +0 -1
  131. package/test/hls/hls_parser_unit.js +45 -1
  132. package/test/media/closed_caption_parser_unit.js +20 -0
  133. package/test/media/drm_engine_unit.js +59 -3
  134. package/test/media/playhead_unit.js +27 -1
  135. package/test/media/presentation_timeline_unit.js +19 -0
  136. package/test/media/segment_index_unit.js +76 -3
  137. package/test/media/streaming_engine_unit.js +3 -3
  138. package/test/player_integration.js +38 -0
  139. package/test/player_unit.js +125 -6
  140. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  141. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  142. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  143. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  144. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  145. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  146. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  147. package/test/test/assets/3675/dash_0.mpd +30 -0
  148. package/test/test/assets/3675/dash_1.mpd +30 -0
  149. package/test/test/assets/3675/dash_2.mpd +30 -0
  150. package/test/test/assets/3675/dash_3.mpd +30 -0
  151. package/test/test/assets/3675/dash_4.mpd +30 -0
  152. package/test/test/assets/3675/dash_5.mpd +26 -0
  153. package/test/test/assets/3675/init-stream0.m4s +0 -0
  154. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  155. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  156. package/test/test/util/fake_ad_manager.js +4 -1
  157. package/test/test/util/simple_fakes.js +5 -3
  158. package/test/test/util/waiter.js +36 -2
  159. package/test/text/ttml_text_parser_unit.js +55 -0
  160. package/test/text/ui_text_displayer_unit.js +95 -18
  161. package/test/text/vtt_text_parser_unit.js +17 -1
  162. package/test/ui/localization_unit.js +15 -14
  163. package/test/ui/text_displayer_layout_unit.js +52 -1
  164. package/test/ui/ui_unit.js +4 -0
  165. package/test/util/mp4_box_parsers_unit.js +28 -0
  166. package/test/util/periods_unit.js +190 -2
  167. package/test/util/stream_utils_unit.js +56 -5
  168. package/ui/airplay_button.js +5 -0
  169. package/ui/big_play_button.js +10 -0
  170. package/ui/cast_button.js +2 -3
  171. package/ui/controls.js +9 -7
  172. package/ui/less/containers.less +10 -5
  173. package/ui/locales/en.json +1 -0
  174. package/ui/locales/ru.json +6 -6
  175. package/ui/locales/source.json +4 -0
  176. package/ui/localization.js +28 -51
  177. package/ui/pip_button.js +2 -3
  178. package/ui/play_button.js +10 -10
  179. package/ui/small_play_button.js +12 -0
  180. package/ui/ui.js +1 -1
  181. 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
  };
@@ -65,6 +65,12 @@ shaka.dash.DashParser = class {
65
65
  */
66
66
  this.segmentIndexMap_ = {};
67
67
 
68
+ /**
69
+ * A map of period ids to their durations
70
+ * @private {!Object.<string, number>}
71
+ */
72
+ this.periodDurations_ = {};
73
+
68
74
  /** @private {shaka.util.PeriodCombiner} */
69
75
  this.periodCombiner_ = new shaka.util.PeriodCombiner();
70
76
 
@@ -89,6 +95,18 @@ shaka.dash.DashParser = class {
89
95
  /** @private {!shaka.util.OperationManager} */
90
96
  this.operationManager_ = new shaka.util.OperationManager();
91
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
+
92
110
  /**
93
111
  * The minimum of the availabilityTimeOffset values among the adaptation
94
112
  * sets.
@@ -527,6 +545,7 @@ shaka.dash.DashParser = class {
527
545
  for (const {i, item: elem, next} of enumerate(periodNodes)) {
528
546
  const start = /** @type {number} */ (
529
547
  XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
548
+ const periodId = elem.id;
530
549
  const givenDuration =
531
550
  XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
532
551
 
@@ -560,6 +579,42 @@ shaka.dash.DashParser = class {
560
579
  periodDuration = givenDuration;
561
580
  }
562
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
+
563
618
  // Parse child nodes.
564
619
  const info = {
565
620
  start: start,
@@ -570,6 +625,10 @@ shaka.dash.DashParser = class {
570
625
  const period = this.parsePeriod_(context, baseUris, info);
571
626
  periods.push(period);
572
627
 
628
+ if (context.period.id && periodDuration) {
629
+ this.periodDurations_[context.period.id] = periodDuration;
630
+ }
631
+
573
632
  if (periodDuration == null) {
574
633
  if (next) {
575
634
  // If the duration is still null and we aren't at the end, then we
@@ -587,6 +646,9 @@ shaka.dash.DashParser = class {
587
646
  prevEnd = start + periodDuration;
588
647
  } // end of period parsing loop
589
648
 
649
+ // Replace previous seen periods with the current one.
650
+ this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
651
+
590
652
  if (presentationDuration != null) {
591
653
  if (prevEnd != presentationDuration) {
592
654
  shaka.log.warning(
@@ -798,6 +860,25 @@ shaka.dash.DashParser = class {
798
860
  }
799
861
  }
800
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
+
801
882
  const essentialProperties =
802
883
  XmlUtils.findChildren(elem, 'EssentialProperty');
803
884
  // ID of real AdaptationSet if this is a trick mode set:
@@ -807,11 +888,26 @@ shaka.dash.DashParser = class {
807
888
  const schemeId = prop.getAttribute('schemeIdUri');
808
889
  if (schemeId == 'http://dashif.org/guidelines/trickmode') {
809
890
  trickModeFor = prop.getAttribute('value');
891
+ } else if (schemeId == videoRangeScheme) {
892
+ videoRange = getVideoRangeFromTransferCharacteristicCICP(
893
+ parseInt(prop.getAttribute('value'), 10),
894
+ );
810
895
  } else {
811
896
  unrecognizedEssentialProperty = true;
812
897
  }
813
898
  }
814
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
+
815
911
  const accessibilities = XmlUtils.findChildren(elem, 'Accessibility');
816
912
  const LanguageUtils = shaka.util.LanguageUtils;
817
913
  const closedCaptions = new Map();
@@ -848,7 +944,9 @@ shaka.dash.DashParser = class {
848
944
  channel = channelAndLanguage[0].startsWith('CC') ?
849
945
  channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
850
946
 
851
- 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';
852
950
  }
853
951
  closedCaptions.set(channel, LanguageUtils.normalize(language));
854
952
  }
@@ -935,8 +1033,13 @@ shaka.dash.DashParser = class {
935
1033
  // Parse Representations into Streams.
936
1034
  const representations = XmlUtils.findChildren(elem, 'Representation');
937
1035
  const streams = representations.map((representation) => {
938
- return this.parseRepresentation_(context, contentProtection, kind,
939
- 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;
940
1043
  }).filter((s) => !!s);
941
1044
 
942
1045
  if (streams.length == 0) {
@@ -1060,7 +1163,7 @@ shaka.dash.DashParser = class {
1060
1163
  const hasManifest = !!this.manifest_;
1061
1164
  streamInfo = shaka.dash.SegmentTemplate.createStreamInfo(
1062
1165
  context, requestInitSegment, this.segmentIndexMap_, hasManifest,
1063
- this.config_.dash.initialSegmentLimit);
1166
+ this.config_.dash.initialSegmentLimit, this.periodDurations_);
1064
1167
  } else {
1065
1168
  goog.asserts.assert(isText,
1066
1169
  'Must have Segment* with non-text streams.');
@@ -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;
@@ -35,11 +35,12 @@ shaka.dash.SegmentTemplate = class {
35
35
  * @param {boolean} isUpdate True if the manifest is being updated.
36
36
  * @param {number} segmentLimit The maximum number of segments to generate for
37
37
  * a SegmentTemplate with fixed duration.
38
+ * @param {!Object.<string, number>} periodDurationMap
38
39
  * @return {shaka.dash.DashParser.StreamInfo}
39
40
  */
40
41
  static createStreamInfo(
41
42
  context, requestInitSegment, segmentIndexMap, isUpdate,
42
- segmentLimit) {
43
+ segmentLimit, periodDurationMap) {
43
44
  goog.asserts.assert(context.representation.segmentTemplate,
44
45
  'Should only be called with SegmentTemplate');
45
46
  const SegmentTemplate = shaka.dash.SegmentTemplate;
@@ -77,7 +78,8 @@ shaka.dash.SegmentTemplate = class {
77
78
  return {
78
79
  generateSegmentIndex: () => {
79
80
  return SegmentTemplate.generateSegmentIndexFromDuration_(
80
- shallowCopyOfContext, info, segmentLimit, initSegmentReference);
81
+ shallowCopyOfContext, info, segmentLimit, initSegmentReference,
82
+ periodDurationMap);
81
83
  },
82
84
  };
83
85
  } else {
@@ -114,12 +116,12 @@ shaka.dash.SegmentTemplate = class {
114
116
  segmentIndex.mergeAndEvict(references,
115
117
  context.presentationTimeline.getSegmentAvailabilityStart());
116
118
  } else {
117
- context.presentationTimeline.notifySegments(references);
118
119
  segmentIndex = new shaka.media.SegmentIndex(references);
119
120
  if (id && context.dynamic) {
120
121
  segmentIndexMap[id] = segmentIndex;
121
122
  }
122
123
  }
124
+ context.presentationTimeline.notifySegments(references);
123
125
 
124
126
  if (shouldFit) {
125
127
  segmentIndex.fit(periodStart, periodEnd);
@@ -259,11 +261,12 @@ shaka.dash.SegmentTemplate = class {
259
261
  * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
260
262
  * @param {number} segmentLimit The maximum number of segments to generate.
261
263
  * @param {shaka.media.InitSegmentReference} initSegmentReference
264
+ * @param {!Object.<string, number>} periodDurationMap
262
265
  * @return {!Promise.<shaka.media.SegmentIndex>}
263
266
  * @private
264
267
  */
265
268
  static generateSegmentIndexFromDuration_(
266
- context, info, segmentLimit, initSegmentReference) {
269
+ context, info, segmentLimit, initSegmentReference, periodDurationMap) {
267
270
  goog.asserts.assert(info.mediaTemplate,
268
271
  'There should be a media template with duration');
269
272
 
@@ -275,9 +278,20 @@ shaka.dash.SegmentTemplate = class {
275
278
  // Capture values that could change as the parsing context moves on to
276
279
  // other parts of the manifest.
277
280
  const periodStart = context.periodInfo.start;
278
- const periodDuration = context.periodInfo.duration;
279
- const periodEnd = periodDuration ?
280
- periodStart + periodDuration : Infinity;
281
+ const periodId = context.period.id;
282
+ const initialPeriodDuration = context.periodInfo.duration;
283
+
284
+ // For multi-period live streams the period duration may not be known until
285
+ // the following period appears in an updated manifest. periodDurationMap
286
+ // provides the updated period duration.
287
+ const getPeriodEnd = () => {
288
+ const periodDuration =
289
+ (periodId != null && periodDurationMap[periodId]) ||
290
+ initialPeriodDuration;
291
+ const periodEnd = periodDuration ?
292
+ (periodStart + periodDuration) : Infinity;
293
+ return periodEnd;
294
+ };
281
295
 
282
296
  const segmentDuration = info.segmentDuration;
283
297
  goog.asserts.assert(
@@ -304,7 +318,7 @@ shaka.dash.SegmentTemplate = class {
304
318
 
305
319
  Math.min(
306
320
  presentationTimeline.getSegmentAvailabilityEnd(),
307
- periodEnd),
321
+ getPeriodEnd()),
308
322
  ];
309
323
  };
310
324
 
@@ -376,16 +390,17 @@ shaka.dash.SegmentTemplate = class {
376
390
 
377
391
  // Relative to the presentation.
378
392
  const segmentStart = segmentPeriodTime + periodStart;
393
+ const trueSegmentEnd = segmentStart + segmentDuration;
379
394
  // Cap the segment end at the period end so that references from the
380
395
  // next period will fit neatly after it.
381
- const segmentEnd = Math.min(segmentStart + segmentDuration, periodEnd);
396
+ const segmentEnd = Math.min(trueSegmentEnd, getPeriodEnd());
382
397
 
383
398
  // This condition will be true unless the segmentStart was >= periodEnd.
384
399
  // If we've done the position calculations correctly, this won't happen.
385
400
  goog.asserts.assert(segmentStart < segmentEnd,
386
401
  'Generated a segment outside of the period!');
387
402
 
388
- return new shaka.media.SegmentReference(
403
+ const ref = new shaka.media.SegmentReference(
389
404
  segmentStart,
390
405
  segmentEnd,
391
406
  getUris,
@@ -394,7 +409,10 @@ shaka.dash.SegmentTemplate = class {
394
409
  initSegmentReference,
395
410
  timestampOffset,
396
411
  /* appendWindowStart= */ periodStart,
397
- /* appendWindowEnd= */ periodEnd);
412
+ /* appendWindowEnd= */ getPeriodEnd());
413
+ // This is necessary information for thumbnail streams:
414
+ ref.trueEndTime = trueSegmentEnd;
415
+ return ref;
398
416
  };
399
417
 
400
418
  for (let position = minPosition; position <= maxPosition; ++position) {
@@ -407,7 +425,15 @@ shaka.dash.SegmentTemplate = class {
407
425
 
408
426
  // If the availability timeline currently ends before the period, we will
409
427
  // need to add references over time.
410
- if (presentationTimeline.getSegmentAvailabilityEnd() < periodEnd) {
428
+ const willNeedToAddReferences =
429
+ presentationTimeline.getSegmentAvailabilityEnd() < getPeriodEnd();
430
+
431
+ // When we start a live stream with a period that ends within the
432
+ // availability window we will not need to add more references, but we will
433
+ // need to evict old references.
434
+ const willNeedToEvictReferences = presentationTimeline.isLive();
435
+
436
+ if (willNeedToAddReferences || willNeedToEvictReferences) {
411
437
  // The period continues to get longer over time, so check for new
412
438
  // references once every |segmentDuration| seconds.
413
439
  // We clamp to |minPosition| in case the initial range was reversed and no
@@ -416,7 +442,9 @@ shaka.dash.SegmentTemplate = class {
416
442
  let nextPosition = Math.max(minPosition, maxPosition + 1);
417
443
  segmentIndex.updateEvery(segmentDuration, () => {
418
444
  // Evict any references outside the window.
419
- segmentIndex.evict(presentationTimeline.getSegmentAvailabilityStart());
445
+ const availabilityStartTime =
446
+ presentationTimeline.getSegmentAvailabilityStart();
447
+ segmentIndex.evict(availabilityStartTime);
420
448
 
421
449
  // Compute any new references that need to be added.
422
450
  const [_, maxPosition] = computeAvailablePositionRange();
@@ -426,8 +454,10 @@ shaka.dash.SegmentTemplate = class {
426
454
  references.push(reference);
427
455
  nextPosition++;
428
456
  }
429
- if (presentationTimeline.getSegmentAvailabilityEnd() >= periodEnd &&
430
- !references.length) {
457
+
458
+ // The timer must continue firing until the entire period is
459
+ // unavailable, so that all references will be evicted.
460
+ if (availabilityStartTime > getPeriodEnd() && !references.length) {
431
461
  // Signal stop.
432
462
  return null;
433
463
  }
@@ -14,7 +14,7 @@ shaka.dependencies = class {
14
14
  * Registers a new dependency.
15
15
  *
16
16
  * @param {shaka.dependencies.Allowed} key which is used for retrieving a
17
- * dependency
17
+ * dependency
18
18
  * @param {?} dep a dependency
19
19
  * @export
20
20
  */
@@ -22,14 +22,15 @@ shaka.dependencies = class {
22
22
  if (!shaka.dependencies.Allowed[key]) {
23
23
  throw new Error(`${key} is not supported`);
24
24
  }
25
- shaka.dependencies.dependencies_.set(key, dep);
25
+ shaka.dependencies.dependencies_.set(key, () => dep);
26
26
  }
27
27
 
28
28
  /**
29
- * Check if we have a dependency for the key
30
- * @export
29
+ * Check if we have a dependency for the key.
30
+ *
31
31
  * @param {shaka.dependencies.Allowed} key key
32
32
  * @return {boolean}
33
+ * @export
33
34
  */
34
35
  static has(key) {
35
36
  return shaka.dependencies.dependencies_.has(key);
@@ -38,16 +39,10 @@ shaka.dependencies = class {
38
39
  /** @return {?muxjs} */
39
40
  static muxjs() {
40
41
  return /** @type {?muxjs} */ (shaka.dependencies.dependencies_.get(
41
- shaka.dependencies.Allowed.muxjs));
42
+ shaka.dependencies.Allowed.muxjs)());
42
43
  }
43
44
  };
44
45
 
45
- /**
46
- * Contains shared dependencies that could be used by other components.
47
- * @private {!Map.<string, Object>}
48
- */
49
- shaka.dependencies.dependencies_ = new Map();
50
-
51
46
  /**
52
47
  * @export
53
48
  * @enum {string}
@@ -56,7 +51,12 @@ shaka.dependencies.Allowed = {
56
51
  muxjs: 'muxjs',
57
52
  };
58
53
 
59
- // Add global muxjs object for backward compatibility
60
- shaka.dependencies.dependencies_.set(
61
- shaka.dependencies.Allowed.muxjs,
62
- window.muxjs);
54
+ /**
55
+ * Contains accessor functions to shared dependencies that could be used by
56
+ * other components. The default accessors can be overridden.
57
+ *
58
+ * @private {!Map.<shaka.dependencies.Allowed, function():?>}
59
+ */
60
+ shaka.dependencies.dependencies_ = new Map([
61
+ [shaka.dependencies.Allowed.muxjs, () => window.muxjs],
62
+ ]);