shaka-player 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/AUTHORS +2 -0
  3. package/CHANGELOG.md +98 -0
  4. package/CODE_OF_CONDUCT.md +1 -1
  5. package/CONTRIBUTORS +2 -0
  6. package/README.md +38 -32
  7. package/demo/common/asset.js +11 -17
  8. package/demo/common/assets.js +117 -11
  9. package/demo/config.js +28 -1
  10. package/demo/custom.js +52 -38
  11. package/demo/main.js +11 -3
  12. package/demo/search.js +7 -0
  13. package/dist/controls.css +1 -1
  14. package/dist/controls.css.map +1 -1
  15. package/dist/demo.compiled.debug.js +133 -124
  16. package/dist/demo.compiled.debug.map +3 -3
  17. package/dist/demo.compiled.js +133 -124
  18. package/dist/demo.compiled.map +3 -3
  19. package/dist/deps.js +20 -18
  20. package/dist/locales.js +35 -31
  21. package/dist/receiver.compiled.debug.js +58 -50
  22. package/dist/receiver.compiled.debug.map +3 -3
  23. package/dist/receiver.compiled.js +58 -50
  24. package/dist/receiver.compiled.map +3 -3
  25. package/dist/shaka-player.compiled.d.ts +440 -390
  26. package/dist/shaka-player.compiled.debug.d.ts +440 -390
  27. package/dist/shaka-player.compiled.debug.externs.js +162 -77
  28. package/dist/shaka-player.compiled.debug.js +786 -736
  29. package/dist/shaka-player.compiled.debug.map +4 -4
  30. package/dist/shaka-player.compiled.externs.js +162 -77
  31. package/dist/shaka-player.compiled.js +1154 -1121
  32. package/dist/shaka-player.compiled.map +4 -4
  33. package/dist/shaka-player.ui.d.ts +491 -437
  34. package/dist/shaka-player.ui.debug.d.ts +491 -437
  35. package/dist/shaka-player.ui.debug.externs.js +167 -77
  36. package/dist/shaka-player.ui.debug.js +877 -811
  37. package/dist/shaka-player.ui.debug.map +4 -4
  38. package/dist/shaka-player.ui.externs.js +167 -77
  39. package/dist/shaka-player.ui.js +1362 -1318
  40. package/dist/shaka-player.ui.map +4 -4
  41. package/docs/tutorials/ad_monetization.md +54 -0
  42. package/docs/tutorials/basic-usage.md +2 -1
  43. package/docs/tutorials/errors.md +2 -1
  44. package/docs/tutorials/offline.md +10 -8
  45. package/docs/tutorials/plugins.md +5 -0
  46. package/docs/tutorials/ui-customization.md +13 -0
  47. package/docs/tutorials/ui.md +2 -1
  48. package/docs/tutorials/upgrade.md +3 -0
  49. package/externs/managedmediasource.js +24 -0
  50. package/externs/shaka/abr_manager.js +6 -2
  51. package/externs/shaka/cea.js +7 -0
  52. package/externs/shaka/codecs.js +5 -3
  53. package/externs/shaka/manifest.js +21 -5
  54. package/externs/shaka/manifest_parser.js +17 -0
  55. package/externs/shaka/net.js +2 -2
  56. package/externs/shaka/offline.js +9 -2
  57. package/externs/shaka/player.js +53 -7
  58. package/externs/shaka/text.js +3 -1
  59. package/externs/shaka/transmuxer.js +1 -1
  60. package/lib/abr/simple_abr_manager.js +25 -8
  61. package/lib/ads/media_tailor_ad_manager.js +14 -3
  62. package/lib/cast/cast_receiver.js +1 -1
  63. package/lib/cea/cea_decoder.js +28 -0
  64. package/lib/cea/dummy_caption_decoder.js +5 -0
  65. package/lib/dash/dash_parser.js +249 -83
  66. package/lib/dash/mpd_utils.js +52 -31
  67. package/lib/dash/segment_base.js +4 -5
  68. package/lib/dash/segment_list.js +5 -4
  69. package/lib/dash/segment_template.js +83 -26
  70. package/lib/hls/hls_classes.js +28 -43
  71. package/lib/hls/hls_parser.js +761 -649
  72. package/lib/hls/hls_utils.js +64 -5
  73. package/lib/hls/manifest_text_parser.js +7 -11
  74. package/lib/media/adaptation_set.js +20 -13
  75. package/lib/media/adaptation_set_criteria.js +70 -18
  76. package/lib/media/closed_caption_parser.js +13 -0
  77. package/lib/media/content_workarounds.js +6 -0
  78. package/lib/media/drm_engine.js +8 -13
  79. package/lib/media/manifest_parser.js +8 -113
  80. package/lib/media/media_source_capabilities.js +10 -3
  81. package/lib/media/media_source_engine.js +75 -21
  82. package/lib/media/play_rate_controller.js +8 -38
  83. package/lib/media/presentation_timeline.js +7 -1
  84. package/lib/media/quality_observer.js +1 -1
  85. package/lib/media/segment_index.js +5 -5
  86. package/lib/media/segment_prefetch.js +92 -24
  87. package/lib/media/segment_reference.js +67 -1
  88. package/lib/media/segment_utils.js +343 -0
  89. package/lib/media/stall_detector.js +0 -3
  90. package/lib/media/streaming_engine.js +41 -21
  91. package/lib/mss/mss_parser.js +18 -2
  92. package/lib/net/networking_engine.js +66 -12
  93. package/lib/net/networking_utils.js +95 -0
  94. package/lib/offline/indexeddb/v1_storage_cell.js +2 -0
  95. package/lib/offline/indexeddb/v2_storage_cell.js +2 -0
  96. package/lib/offline/manifest_converter.js +2 -0
  97. package/lib/offline/offline_manifest_parser.js +10 -0
  98. package/lib/offline/storage.js +11 -7
  99. package/lib/player.js +993 -1502
  100. package/lib/polyfill/mediasource.js +22 -1
  101. package/lib/text/mp4_ttml_parser.js +2 -2
  102. package/lib/text/srt_text_parser.js +2 -2
  103. package/lib/text/stub_text_displayer.js +51 -0
  104. package/lib/text/text_engine.js +3 -2
  105. package/lib/text/ttml_text_parser.js +28 -5
  106. package/lib/text/ui_text_displayer.js +4 -0
  107. package/lib/text/vtt_text_parser.js +7 -2
  108. package/lib/transmuxer/aac_transmuxer.js +1 -1
  109. package/lib/transmuxer/ac3_transmuxer.js +1 -1
  110. package/lib/transmuxer/ec3_transmuxer.js +1 -1
  111. package/lib/transmuxer/h264.js +5 -4
  112. package/lib/transmuxer/h265.js +5 -4
  113. package/lib/transmuxer/mp3_transmuxer.js +1 -1
  114. package/lib/transmuxer/mpeg_ts_transmuxer.js +1 -1
  115. package/lib/transmuxer/mss_transmuxer.js +1 -1
  116. package/lib/transmuxer/ts_transmuxer.js +33 -13
  117. package/lib/util/buffer_utils.js +26 -4
  118. package/lib/util/content_steering_manager.js +358 -0
  119. package/lib/util/error.js +2 -6
  120. package/lib/util/fairplay_utils.js +1 -1
  121. package/lib/util/fake_event.js +5 -1
  122. package/lib/util/id3_utils.js +50 -1
  123. package/lib/util/manifest_parser_utils.js +12 -8
  124. package/lib/util/mp4_generator.js +2 -4
  125. package/lib/util/mutex.js +59 -0
  126. package/lib/util/object_utils.js +1 -1
  127. package/lib/util/periods.js +154 -179
  128. package/lib/util/platform.js +4 -3
  129. package/lib/util/player_configuration.js +35 -6
  130. package/lib/util/stream_utils.js +105 -98
  131. package/lib/util/ts_parser.js +124 -73
  132. package/lib/util/uint8array_utils.js +5 -1
  133. package/lib/util/xml_utils.js +5 -0
  134. package/maintained-branches.md +2 -2
  135. package/package.json +1 -1
  136. package/roadmap.md +15 -5
  137. package/shaka-player.uncompiled.js +0 -1
  138. package/test/abr/simple_abr_manager_unit.js +33 -33
  139. package/test/ads/ad_manager_unit.js +3 -2
  140. package/test/cast/cast_receiver_integration.js +3 -4
  141. package/test/cast/cast_utils_unit.js +2 -0
  142. package/test/cea/cea_decoder_unit.js +7 -0
  143. package/test/codec_switching/codec_switching_integration.js +2 -1
  144. package/test/dash/dash_parser_integration.js +2 -1
  145. package/test/dash/dash_parser_live_unit.js +0 -19
  146. package/test/dash/dash_parser_manifest_unit.js +494 -2
  147. package/test/dash/mpd_utils_unit.js +61 -30
  148. package/test/hls/hls_live_unit.js +12 -3
  149. package/test/hls/hls_parser_unit.js +377 -27
  150. package/test/hls/manifest_text_parser_unit.js +32 -99
  151. package/test/media/adaptation_set_criteria_unit.js +172 -21
  152. package/test/media/adaptation_set_unit.js +1 -0
  153. package/test/media/content_workarounds_unit.js +1 -1
  154. package/test/media/drm_engine_unit.js +3 -3
  155. package/test/media/media_source_engine_unit.js +1 -1
  156. package/test/media/play_rate_controller_unit.js +0 -48
  157. package/test/media/segment_index_unit.js +2 -1
  158. package/test/mss/mss_player_integration.js +3 -2
  159. package/test/net/networking_engine_unit.js +3 -3
  160. package/test/{media/manifest_parser_unit.js → net/networking_utils_unit.js} +2 -2
  161. package/test/offline/manifest_convert_unit.js +9 -0
  162. package/test/offline/offline_integration.js +2 -1
  163. package/test/offline/storage_integration.js +21 -9
  164. package/test/player_external.js +4 -2
  165. package/test/player_integration.js +15 -4
  166. package/test/player_load_graph_integration.js +47 -151
  167. package/test/player_src_equals_external.js +1 -0
  168. package/test/player_src_equals_integration.js +71 -4
  169. package/test/player_unit.js +80 -27
  170. package/test/test/util/fake_media_source_engine.js +5 -0
  171. package/test/test/util/fake_networking_engine.js +9 -1
  172. package/test/test/util/fake_segment_prefetch.js +3 -0
  173. package/test/test/util/manifest_generator.js +4 -0
  174. package/test/test/util/manifest_parser_util.js +14 -2
  175. package/test/test/util/offline_utils.js +1 -0
  176. package/test/test/util/simple_fakes.js +2 -2
  177. package/test/test/util/streaming_engine_util.js +3 -0
  178. package/test/test/util/test_scheme.js +6 -0
  179. package/test/test/util/ui_utils.js +4 -3
  180. package/test/text/mp4_ttml_parser_unit.js +4 -4
  181. package/test/text/srt_text_parser_unit.js +1 -1
  182. package/test/text/text_displayer_layout_unit.js +1 -1
  183. package/test/text/text_engine_unit.js +6 -0
  184. package/test/text/ttml_text_parser_unit.js +33 -2
  185. package/test/transmuxer/transmuxer_engine_integration.js +9 -0
  186. package/test/transmuxer/transmuxer_integration.js +33 -3
  187. package/test/ui/ad_ui_unit.js +2 -2
  188. package/test/ui/ui_customization_unit.js +17 -16
  189. package/test/ui/ui_integration.js +30 -20
  190. package/test/ui/ui_unit.js +25 -19
  191. package/test/util/buffer_utils_unit.js +1 -1
  192. package/test/util/content_steering_manager_unit.js +427 -0
  193. package/test/util/id3_utils_unit.js +52 -0
  194. package/test/util/stream_utils_unit.js +17 -113
  195. package/test/util/ts_parser_unit.js +3 -3
  196. package/third_party/closure-uri/uri.js +13 -1
  197. package/ui/audio_language_selection.js +9 -2
  198. package/ui/externs/ui.js +17 -2
  199. package/ui/language_utils.js +43 -8
  200. package/ui/less/range_elements.less +33 -0
  201. package/ui/locales/en.json +1 -0
  202. package/ui/locales/es-419.json +1 -0
  203. package/ui/locales/es.json +1 -0
  204. package/ui/locales/source.json +4 -0
  205. package/ui/localization.js +10 -0
  206. package/ui/remote_button.js +3 -1
  207. package/ui/resolution_selection.js +47 -10
  208. package/ui/seek_bar.js +296 -3
  209. package/ui/text_selection.js +2 -1
  210. package/ui/ui.js +6 -2
  211. package/lib/routing/node.js +0 -23
  212. package/lib/routing/payload.js +0 -47
  213. package/lib/routing/walker.js +0 -587
  214. package/test/routing/walker_unit.js +0 -505
@@ -26,18 +26,16 @@ goog.require('shaka.net.DataUriPlugin');
26
26
  goog.require('shaka.net.NetworkingEngine');
27
27
  goog.require('shaka.util.ArrayUtils');
28
28
  goog.require('shaka.util.BufferUtils');
29
+ goog.require('shaka.util.ContentSteeringManager');
29
30
  goog.require('shaka.util.Error');
30
31
  goog.require('shaka.util.FakeEvent');
31
- goog.require('shaka.util.Functional');
32
32
  goog.require('shaka.util.LanguageUtils');
33
33
  goog.require('shaka.util.ManifestParserUtils');
34
34
  goog.require('shaka.util.MimeUtils');
35
- goog.require('shaka.util.Mp4BoxParsers');
36
- goog.require('shaka.util.Mp4Parser');
37
35
  goog.require('shaka.util.OperationManager');
38
36
  goog.require('shaka.util.Pssh');
37
+ goog.require('shaka.media.SegmentUtils');
39
38
  goog.require('shaka.util.Timer');
40
- goog.require('shaka.util.TsParser');
41
39
  goog.require('shaka.util.Platform');
42
40
  goog.require('shaka.util.Uint8ArrayUtils');
43
41
  goog.require('shaka.util.XmlUtils');
@@ -112,8 +110,8 @@ shaka.hls.HlsParser = class {
112
110
  * timestamps, offsets, and to handle TS rollover.
113
111
  *
114
112
  * During parsing, used to avoid duplicates in the async methods
115
- * createStreamInfoFromMediaTag_, createStreamInfoFromImageTag_ and
116
- * createStreamInfoFromVariantTag_.
113
+ * createStreamInfoFromMediaTags_, createStreamInfoFromImageTag_ and
114
+ * createStreamInfoFromVariantTags_.
117
115
  *
118
116
  * @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
119
117
  */
@@ -157,6 +155,13 @@ shaka.hls.HlsParser = class {
157
155
  */
158
156
  this.streamsFinalized_ = false;
159
157
 
158
+ /**
159
+ * Whether the manifest informs about the codec to use.
160
+ *
161
+ * @private
162
+ */
163
+ this.codecInfoInManifest_ = false;
164
+
160
165
  /**
161
166
  * This timer is used to trigger the start of a manifest update. A manifest
162
167
  * update is async. Once the update is finished, the timer will be restarted
@@ -223,6 +228,9 @@ shaka.hls.HlsParser = class {
223
228
  * @private {!shaka.abr.Ewma}
224
229
  */
225
230
  this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
231
+
232
+ /** @private {?shaka.util.ContentSteeringManager} */
233
+ this.contentSteeringManager_ = null;
226
234
  }
227
235
 
228
236
 
@@ -232,6 +240,10 @@ shaka.hls.HlsParser = class {
232
240
  */
233
241
  configure(config) {
234
242
  this.config_ = config;
243
+
244
+ if (this.contentSteeringManager_) {
245
+ this.contentSteeringManager_.configure(this.config_);
246
+ }
235
247
  }
236
248
 
237
249
  /**
@@ -243,7 +255,7 @@ shaka.hls.HlsParser = class {
243
255
  this.playerInterface_ = playerInterface;
244
256
  this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
245
257
 
246
- const response = await this.requestManifest_(uri);
258
+ const response = await this.requestManifest_([uri]);
247
259
 
248
260
  // Record the master playlist URI after redirects.
249
261
  this.masterPlaylistUri_ = response.uri;
@@ -284,6 +296,10 @@ shaka.hls.HlsParser = class {
284
296
  this.groupIdToCodecsMap_.clear();
285
297
  this.globalVariables_.clear();
286
298
 
299
+ if (this.contentSteeringManager_) {
300
+ this.contentSteeringManager_.destroy();
301
+ }
302
+
287
303
  return Promise.all(pending);
288
304
  }
289
305
 
@@ -354,40 +370,42 @@ shaka.hls.HlsParser = class {
354
370
  * @private
355
371
  */
356
372
  async updateStream_(streamInfo) {
357
- const manifestUri = streamInfo.absoluteMediaPlaylistUri;
358
- const uriObj = new goog.Uri(manifestUri);
359
- const queryData = new goog.Uri.QueryData();
360
- if (streamInfo.canBlockReload) {
361
- if (streamInfo.nextMediaSequence >= 0) {
362
- // Indicates that the server must hold the request until a Playlist
363
- // contains a Media Segment with Media Sequence
364
- queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence));
373
+ const manifestUris = [];
374
+ for (const uri of streamInfo.getUris()) {
375
+ const uriObj = new goog.Uri(uri);
376
+ const queryData = uriObj.getQueryData();
377
+ if (streamInfo.canBlockReload) {
378
+ if (streamInfo.nextMediaSequence >= 0) {
379
+ // Indicates that the server must hold the request until a Playlist
380
+ // contains a Media Segment with Media Sequence
381
+ queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence));
382
+ }
383
+ if (streamInfo.nextPart >= 0) {
384
+ // Indicates, in combination with _HLS_msn, that the server must hold
385
+ // the request until a Playlist contains Partial Segment N of Media
386
+ // Sequence Number M or later.
387
+ queryData.add('_HLS_part', String(streamInfo.nextPart));
388
+ }
365
389
  }
366
- if (streamInfo.nextPart >= 0) {
367
- // Indicates, in combination with _HLS_msn, that the server must hold
368
- // the request until a Playlist contains Partial Segment N of Media
369
- // Sequence Number M or later.
370
- queryData.add('_HLS_part', String(streamInfo.nextPart));
390
+ if (streamInfo.canSkipSegments) {
391
+ // Enable delta updates. This will replace older segments with
392
+ // 'EXT-X-SKIP' tag in the media playlist.
393
+ queryData.add('_HLS_skip', 'YES');
371
394
  }
372
- }
373
- if (streamInfo.canSkipSegments) {
374
- // Enable delta updates. This will replace older segments with
375
- // 'EXT-X-SKIP' tag in the media playlist.
376
- queryData.add('_HLS_skip', 'YES');
377
- }
378
- if (queryData.getCount()) {
379
- uriObj.setQueryData(queryData);
395
+ if (queryData.getCount()) {
396
+ uriObj.setQueryData(queryData);
397
+ }
398
+ manifestUris.push(uriObj.toString());
380
399
  }
381
400
  const response =
382
- await this.requestManifest_(uriObj.toString(), /* isPlaylist= */ true);
401
+ await this.requestManifest_(manifestUris, /* isPlaylist= */ true);
383
402
  if (!streamInfo.stream.segmentIndex) {
384
403
  // The stream was closed since the update was first requested.
385
404
  return;
386
405
  }
387
406
 
388
407
  /** @type {shaka.hls.Playlist} */
389
- const playlist = this.manifestTextParser_.parsePlaylist(
390
- response.data, response.uri);
408
+ const playlist = this.manifestTextParser_.parsePlaylist(response.data);
391
409
 
392
410
  if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
393
411
  throw new shaka.util.Error(
@@ -396,11 +414,19 @@ shaka.hls.HlsParser = class {
396
414
  shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
397
415
  }
398
416
 
417
+ // Record the final URI after redirects.
418
+ const responseUri = response.uri;
419
+ if (responseUri != response.originalUri &&
420
+ !streamInfo.getUris().includes(responseUri)) {
421
+ streamInfo.redirectUris.push(responseUri);
422
+ }
423
+
399
424
  /** @type {!Array.<!shaka.hls.Tag>} */
400
425
  const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
401
426
  'EXT-X-DEFINE');
402
427
 
403
- const mediaVariables = this.parseMediaVariables_(variablesTags);
428
+ const mediaVariables = this.parseMediaVariables_(
429
+ variablesTags, responseUri);
404
430
 
405
431
  const stream = streamInfo.stream;
406
432
 
@@ -418,8 +444,8 @@ shaka.hls.HlsParser = class {
418
444
  }
419
445
 
420
446
  const {segments, bandwidth} = this.createSegments_(
421
- streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
422
- stream.mimeType, mediaSequenceToStartTime, mediaVariables);
447
+ playlist, stream, mediaSequenceToStartTime, mediaVariables,
448
+ streamInfo.getUris);
423
449
 
424
450
  stream.bandwidth = bandwidth;
425
451
 
@@ -474,6 +500,24 @@ shaka.hls.HlsParser = class {
474
500
  // No-op
475
501
  }
476
502
 
503
+ /**
504
+ * @override
505
+ * @exportInterface
506
+ */
507
+ onInitialVariantChosen(variant) {
508
+ // No-op
509
+ }
510
+
511
+ /**
512
+ * @override
513
+ * @exportInterface
514
+ */
515
+ banLocation(uri) {
516
+ if (this.contentSteeringManager_) {
517
+ this.contentSteeringManager_.banLocation(uri);
518
+ }
519
+ }
520
+
477
521
  /**
478
522
  * Align the streams by sequence number by dropping early segments. Then
479
523
  * offset the streams to begin at presentation time 0.
@@ -599,7 +643,7 @@ shaka.hls.HlsParser = class {
599
643
  const segment0 = segmentIndex.earliestReference();
600
644
  if (segment0.syncTime == null) {
601
645
  shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
602
- streamInfo.verbatimMediaPlaylistUri,
646
+ streamInfo.getUris(),
603
647
  'Expect AV sync issues!');
604
648
  } else {
605
649
  // Stream metadata are offset by a fixed amount based on the
@@ -640,7 +684,7 @@ shaka.hls.HlsParser = class {
640
684
  }
641
685
 
642
686
  shaka.log.debug('Offset', offset, 'applied to',
643
- streamInfo.verbatimMediaPlaylistUri);
687
+ streamInfo.getUris());
644
688
  }
645
689
 
646
690
  /**
@@ -657,14 +701,11 @@ shaka.hls.HlsParser = class {
657
701
  goog.asserts.assert(this.masterPlaylistUri_,
658
702
  'Master playlist URI must be set before calling parseManifest_!');
659
703
 
660
- const playlist = this.manifestTextParser_.parsePlaylist(
661
- data, this.masterPlaylistUri_);
704
+ const playlist = this.manifestTextParser_.parsePlaylist(data);
662
705
 
663
706
  /** @type {!Array.<!shaka.hls.Tag>} */
664
707
  const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
665
708
 
666
- this.parseMasterVariables_(variablesTags);
667
-
668
709
  /** @type {!Array.<!shaka.extern.Variant>} */
669
710
  let variants = [];
670
711
  /** @type {!Array.<!shaka.extern.Stream>} */
@@ -674,9 +715,14 @@ shaka.hls.HlsParser = class {
674
715
 
675
716
  // Parsing a media playlist results in a single-variant stream.
676
717
  if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
718
+ const getUris = () => {
719
+ return [uri];
720
+ };
721
+
677
722
  // Get necessary info for this stream. These are things we would normally
678
723
  // find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
679
- const basicInfo = await this.getMediaPlaylistBasicInfo_(playlist);
724
+ const basicInfo =
725
+ await this.getMediaPlaylistBasicInfo_(playlist, getUris);
680
726
  const type = basicInfo.type;
681
727
  const mimeType = basicInfo.mimeType;
682
728
  const codecs = basicInfo.codecs;
@@ -685,6 +731,7 @@ shaka.hls.HlsParser = class {
685
731
  const width = basicInfo.width;
686
732
  const channelsCount = basicInfo.channelCount;
687
733
  const sampleRate = basicInfo.sampleRate;
734
+ const closedCaptions = basicInfo.closedCaptions;
688
735
 
689
736
  // Some values we cannot figure out, and aren't important enough to ask
690
737
  // the user to provide through config values. A lot of these are only
@@ -692,21 +739,21 @@ shaka.hls.HlsParser = class {
692
739
  // So these unknowns should be set to false or null, largely.
693
740
  const spatialAudio = false;
694
741
  const characteristics = null;
695
- const closedCaptions = new Map();
696
742
  const forced = false; // Only relevant for text.
697
743
  const primary = true; // This is the only stream!
698
744
  const name = 'Media Playlist';
699
745
 
700
746
  // Make the stream info, with those values.
701
747
  const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
702
- playlist, uri, uri, codecs, type, languageValue, primary, name,
703
- channelsCount, closedCaptions, characteristics, forced, sampleRate,
704
- spatialAudio, mimeType);
748
+ this.globalId_++, playlist, getUris, uri, codecs, type,
749
+ languageValue, primary, name, channelsCount, closedCaptions,
750
+ characteristics, forced, sampleRate, spatialAudio, mimeType);
705
751
  this.uriToStreamInfosMap_.set(uri, streamInfo);
706
752
 
707
753
  if (type == 'video') {
708
754
  this.addVideoAttributes_(streamInfo.stream, width, height,
709
- /* frameRate= */ null, /* videoRange= */ null);
755
+ /* frameRate= */ null, /* videoRange= */ null,
756
+ /* videoLayout= */ null);
710
757
  }
711
758
 
712
759
  // Wrap the stream from that stream info with a variant.
@@ -723,8 +770,11 @@ shaka.hls.HlsParser = class {
723
770
  decodingInfos: [],
724
771
  });
725
772
  } else {
773
+ this.parseMasterVariables_(variablesTags);
774
+
726
775
  /** @type {!Array.<!shaka.hls.Tag>} */
727
- const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
776
+ const mediaTags = Utils.filterTagsByName(
777
+ playlist.tags, 'EXT-X-MEDIA');
728
778
  /** @type {!Array.<!shaka.hls.Tag>} */
729
779
  const variantTags = Utils.filterTagsByName(
730
780
  playlist.tags, 'EXT-X-STREAM-INF');
@@ -732,43 +782,28 @@ shaka.hls.HlsParser = class {
732
782
  const imageTags = Utils.filterTagsByName(
733
783
  playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
734
784
  /** @type {!Array.<!shaka.hls.Tag>} */
785
+ const iFrameTags = Utils.filterTagsByName(
786
+ playlist.tags, 'EXT-X-I-FRAME-STREAM-INF');
787
+ /** @type {!Array.<!shaka.hls.Tag>} */
735
788
  const sessionKeyTags = Utils.filterTagsByName(
736
789
  playlist.tags, 'EXT-X-SESSION-KEY');
790
+ /** @type {!Array.<!shaka.hls.Tag>} */
791
+ const sessionDataTags = Utils.filterTagsByName(
792
+ playlist.tags, 'EXT-X-SESSION-DATA');
793
+ /** @type {!Array.<!shaka.hls.Tag>} */
794
+ const contentSteeringTags = Utils.filterTagsByName(
795
+ playlist.tags, 'EXT-X-CONTENT-STEERING');
737
796
 
738
- this.parseCodecs_(variantTags);
797
+ this.processSessionData_(sessionDataTags);
798
+ await this.processContentSteering_(contentSteeringTags);
739
799
 
740
- /** @type {!Array.<!shaka.hls.Tag>} */
741
- const sesionDataTags =
742
- Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA');
743
- for (const tag of sesionDataTags) {
744
- const id = tag.getAttributeValue('DATA-ID');
745
- const uri = tag.getAttributeValue('URI');
746
- const language = tag.getAttributeValue('LANGUAGE');
747
- const value = tag.getAttributeValue('VALUE');
748
- const data = (new Map()).set('id', id);
749
- if (uri) {
750
- data.set('uri', shaka.hls.Utils.constructAbsoluteUri(
751
- this.masterPlaylistUri_, uri));
752
- }
753
- if (language) {
754
- data.set('language', language);
755
- }
756
- if (value) {
757
- data.set('value', value);
758
- }
759
- const event = new shaka.util.FakeEvent('sessiondata', data);
760
- if (this.playerInterface_) {
761
- this.playerInterface_.onEvent(event);
762
- }
763
- }
800
+ this.parseCodecs_(variantTags);
764
801
 
765
- // Parse audio and video media tags first, so that we can extract segment
766
- // start time from audio/video streams and reuse for text streams.
767
- this.createStreamInfosFromMediaTags_(mediaTags);
768
802
  this.parseClosedCaptions_(mediaTags);
769
- variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
803
+ variants =
804
+ this.createVariantsForTags_(variantTags, sessionKeyTags, mediaTags);
770
805
  textStreams = this.parseTexts_(mediaTags);
771
- imageStreams = await this.parseImages_(imageTags);
806
+ imageStreams = await this.parseImages_(imageTags, iFrameTags);
772
807
  }
773
808
 
774
809
  // Make sure that the parser has not been destroyed.
@@ -817,64 +852,72 @@ shaka.hls.HlsParser = class {
817
852
  type: shaka.media.ManifestParser.HLS,
818
853
  serviceDescription: null,
819
854
  };
855
+
856
+ // If there is no 'CODECS' attribute in the manifest and codec guessing is
857
+ // disabled, we need to create the segment indexes now so that missing info
858
+ // can be parsed from the media data and added to the stream objects.
859
+ if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
860
+ const createIndexes = [];
861
+ for (const variant of this.manifest_.variants) {
862
+ if (variant.audio && variant.audio.codecs === '') {
863
+ createIndexes.push(variant.audio.createSegmentIndex());
864
+ }
865
+ if (variant.video && variant.video.codecs === '') {
866
+ createIndexes.push(variant.video.createSegmentIndex());
867
+ }
868
+ }
869
+
870
+ await Promise.all(createIndexes);
871
+ }
872
+
820
873
  this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
821
874
  }
822
875
 
823
876
  /**
824
877
  * @param {shaka.hls.Playlist} playlist
825
- * @return {!Promise.<shaka.hls.HlsParser.BasicInfo>}
878
+ * @param {function():!Array.<string>} getUris
879
+ * @return {!Promise.<shaka.media.SegmentUtils.BasicInfo>}
826
880
  * @private
827
881
  */
828
- async getMediaPlaylistBasicInfo_(playlist) {
882
+ async getMediaPlaylistBasicInfo_(playlist, getUris) {
829
883
  const HlsParser = shaka.hls.HlsParser;
830
- const defaultFullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
831
- const defaultMimeType =
832
- shaka.util.MimeUtils.getBasicType(defaultFullMimeType);
833
- const defaultType = defaultMimeType.split('/')[0];
834
- const defaultCodecs = shaka.util.MimeUtils.getCodecs(defaultFullMimeType);
835
- const defaultBasicInfo = {
836
- type: defaultType,
837
- mimeType: defaultMimeType,
838
- codecs: defaultCodecs,
839
- language: null,
840
- height: null,
841
- width: null,
842
- channelCount: null,
843
- sampleRate: null,
844
- };
884
+ const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType(
885
+ this.config_.hls.mediaPlaylistFullMimeType);
845
886
  if (!playlist.segments.length) {
846
887
  return defaultBasicInfo;
847
888
  }
848
- const firstSegment = playlist.segments[0];
849
- const parsedUri = new goog.Uri(firstSegment.absoluteUri);
889
+ const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
890
+ const middleSegment = playlist.segments[middleSegmentIdx];
891
+ const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
892
+ getUris(),
893
+ playlist.segments[0].verbatimSegmentUri);
894
+ const middleSegmentUri = middleSegmentUris[0];
895
+ const parsedUri = new goog.Uri(middleSegmentUri);
850
896
  const extension = parsedUri.getPath().split('.').pop();
851
897
  const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
852
898
  if (rawMimeType) {
853
- return {
854
- type: 'audio',
855
- mimeType: rawMimeType,
856
- codecs: '',
857
- language: null,
858
- height: null,
859
- width: null,
860
- channelCount: null,
861
- sampleRate: null,
862
- };
899
+ return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
900
+ rawMimeType);
863
901
  }
864
902
 
865
- let segmentUris = [firstSegment.absoluteUri];
903
+ const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
904
+
905
+ let initData = null;
866
906
  const initSegmentRef = this.getInitSegmentReference_(
867
- playlist, firstSegment.tags, new Map());
907
+ playlist, middleSegment.tags, getUris);
868
908
  if (initSegmentRef) {
869
- segmentUris = initSegmentRef.getUris();
909
+ const initSegmentRequest = shaka.net.NetworkingEngine.makeRequest(
910
+ initSegmentRef.getUris(), this.config_.retryParameters);
911
+ const initType =
912
+ shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
913
+ const initResponse = await this.makeNetworkRequest_(
914
+ initSegmentRequest, requestType, {type: initType});
915
+ initData = initResponse.data;
870
916
  }
871
917
 
872
- const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
873
918
  const segmentRequest = shaka.net.NetworkingEngine.makeRequest(
874
- segmentUris, this.config_.retryParameters);
875
- const type = initSegmentRef ?
876
- shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT :
877
- shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
919
+ middleSegmentUris, this.config_.retryParameters);
920
+ const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
878
921
  const response = await this.makeNetworkRequest_(
879
922
  segmentRequest, requestType, {type});
880
923
 
@@ -885,13 +928,16 @@ shaka.hls.HlsParser = class {
885
928
  }
886
929
 
887
930
  if (extension == 'ts' || contentMimeType == 'video/mp2t') {
888
- const basicInfo = this.getBasicInfoFromTs_(response);
931
+ const basicInfo =
932
+ shaka.media.SegmentUtils.getBasicInfoFromTs(response.data);
889
933
  if (basicInfo) {
890
934
  return basicInfo;
891
935
  }
892
- } else if (extension == 'mp4' ||
893
- contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4') {
894
- const basicInfo = this.getBasicInfoFromMp4_(response);
936
+ } else if (extension == 'mp4' || extension == 'cmfv' ||
937
+ contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4' ||
938
+ contentMimeType == 'video/iso.segment') {
939
+ const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
940
+ initData, response.data);
895
941
  if (basicInfo) {
896
942
  return basicInfo;
897
943
  }
@@ -899,236 +945,6 @@ shaka.hls.HlsParser = class {
899
945
  return defaultBasicInfo;
900
946
  }
901
947
 
902
- /**
903
- * @param {shaka.extern.Response} response
904
- * @return {?shaka.hls.HlsParser.BasicInfo}
905
- * @private
906
- */
907
- getBasicInfoFromTs_(response) {
908
- const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data);
909
- const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
910
- const tsCodecs = tsParser.getCodecs();
911
- const videoInfo = tsParser.getVideoInfo();
912
- const codecs = [];
913
- let hasAudio = false;
914
- let hasVideo = false;
915
- switch (tsCodecs.audio) {
916
- case 'aac':
917
- codecs.push('mp4a.40.2');
918
- hasAudio = true;
919
- break;
920
- case 'mp3':
921
- codecs.push('mp4a.40.34');
922
- hasAudio = true;
923
- break;
924
- case 'ac3':
925
- codecs.push('ac-3');
926
- hasAudio = true;
927
- break;
928
- case 'ec3':
929
- codecs.push('ec-3');
930
- hasAudio = true;
931
- break;
932
- }
933
- switch (tsCodecs.video) {
934
- case 'avc':
935
- if (videoInfo.codec) {
936
- codecs.push(videoInfo.codec);
937
- } else {
938
- codecs.push('avc1.42E01E');
939
- }
940
- hasVideo = true;
941
- break;
942
- case 'hvc':
943
- if (videoInfo.codec) {
944
- codecs.push(videoInfo.codec);
945
- } else {
946
- codecs.push('hvc1.1.6.L93.90');
947
- }
948
- hasVideo = true;
949
- break;
950
- }
951
- if (!codecs.length) {
952
- return null;
953
- }
954
- const onlyAudio = hasAudio && !hasVideo;
955
- return {
956
- type: onlyAudio ? 'audio' : 'video',
957
- mimeType: 'video/mp2t',
958
- codecs: codecs.join(', '),
959
- language: null,
960
- height: videoInfo.height,
961
- width: videoInfo.width,
962
- channelCount: null,
963
- sampleRate: null,
964
- };
965
- }
966
-
967
- /**
968
- * @param {shaka.extern.Response} response
969
- * @return {?shaka.hls.HlsParser.BasicInfo}
970
- * @private
971
- */
972
- getBasicInfoFromMp4_(response) {
973
- const Mp4Parser = shaka.util.Mp4Parser;
974
-
975
- const codecs = [];
976
-
977
- let hasAudio = false;
978
- let hasVideo = false;
979
-
980
- const addCodec = (codec) => {
981
- const codecLC = codec.toLowerCase();
982
- switch (codecLC) {
983
- case 'avc1':
984
- case 'avc3':
985
- codecs.push(codecLC + '.42E01E');
986
- hasVideo = true;
987
- break;
988
- case 'hev1':
989
- case 'hvc1':
990
- codecs.push(codecLC + '.1.6.L93.90');
991
- hasVideo = true;
992
- break;
993
- case 'dvh1':
994
- case 'dvhe':
995
- codecs.push(codecLC + '.05.04');
996
- hasVideo = true;
997
- break;
998
- case 'vp09':
999
- codecs.push(codecLC + '.00.10.08');
1000
- hasVideo = true;
1001
- break;
1002
- case 'av01':
1003
- codecs.push(codecLC + '.0.01M.08');
1004
- hasVideo = true;
1005
- break;
1006
- case 'mp4a':
1007
- // We assume AAC, but this can be wrong since mp4a supports
1008
- // others codecs
1009
- codecs.push('mp4a.40.2');
1010
- hasAudio = true;
1011
- break;
1012
- case 'ac-3':
1013
- case 'ec-3':
1014
- case 'opus':
1015
- case 'flac':
1016
- codecs.push(codecLC);
1017
- hasAudio = true;
1018
- break;
1019
- }
1020
- };
1021
-
1022
- const codecBoxParser = (box) => addCodec(box.name);
1023
-
1024
- /** @type {?string} */
1025
- let language = null;
1026
- /** @type {?string} */
1027
- let height = null;
1028
- /** @type {?string} */
1029
- let width = null;
1030
- /** @type {?number} */
1031
- let channelCount = null;
1032
- /** @type {?number} */
1033
- let sampleRate = null;
1034
-
1035
- new Mp4Parser()
1036
- .box('moov', Mp4Parser.children)
1037
- .box('trak', Mp4Parser.children)
1038
- .fullBox('tkhd', (box) => {
1039
- goog.asserts.assert(
1040
- box.version != null,
1041
- 'TKHD is a full box and should have a valid version.');
1042
- const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
1043
- box.reader, box.version);
1044
- height = String(parsedTKHDBox.height);
1045
- width = String(parsedTKHDBox.width);
1046
- })
1047
- .box('mdia', Mp4Parser.children)
1048
- .fullBox('mdhd', (box) => {
1049
- goog.asserts.assert(
1050
- box.version != null,
1051
- 'MDHD is a full box and should have a valid version.');
1052
- const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
1053
- box.reader, box.version);
1054
- language = parsedMDHDBox.language;
1055
- })
1056
- .box('minf', Mp4Parser.children)
1057
- .box('stbl', Mp4Parser.children)
1058
- .fullBox('stsd', Mp4Parser.sampleDescription)
1059
-
1060
- // AUDIO
1061
- // These are the various boxes that signal a codec.
1062
- .box('mp4a', (box) => {
1063
- const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader);
1064
- channelCount = parsedMP4ABox.channelCount;
1065
- sampleRate = parsedMP4ABox.sampleRate;
1066
- if (box.reader.hasMoreData()) {
1067
- Mp4Parser.children(box);
1068
- } else {
1069
- codecBoxParser(box);
1070
- }
1071
- })
1072
- .box('esds', (box) => {
1073
- const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader);
1074
- codecs.push(parsedESDSBox.codec);
1075
- hasAudio = true;
1076
- })
1077
- .box('ac-3', codecBoxParser)
1078
- .box('ec-3', codecBoxParser)
1079
- .box('opus', codecBoxParser)
1080
- .box('Opus', codecBoxParser)
1081
- .box('fLaC', codecBoxParser)
1082
-
1083
- // VIDEO
1084
- // These are the various boxes that signal a codec.
1085
- .box('avc1', (box) => {
1086
- const parsedAVCBox =
1087
- shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
1088
- codecs.push(parsedAVCBox.codec);
1089
- hasVideo = true;
1090
- })
1091
- .box('avc3', (box) => {
1092
- const parsedAVCBox =
1093
- shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
1094
- codecs.push(parsedAVCBox.codec);
1095
- hasVideo = true;
1096
- })
1097
- .box('hev1', codecBoxParser)
1098
- .box('hvc1', codecBoxParser)
1099
- .box('dvh1', codecBoxParser)
1100
- .box('dvhe', codecBoxParser)
1101
- .box('vp09', codecBoxParser)
1102
- .box('av01', codecBoxParser)
1103
-
1104
- // This signals an encrypted sample, which we can go inside of to
1105
- // find the codec used.
1106
- // Note: If encrypted, you can only have audio or video, not both.
1107
- .box('enca', Mp4Parser.visualSampleEntry)
1108
- .box('encv', Mp4Parser.visualSampleEntry)
1109
- .box('sinf', Mp4Parser.children)
1110
- .box('frma', (box) => {
1111
- const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
1112
- addCodec(codec);
1113
- })
1114
-
1115
- .parse(response.data, /* partialOkay= */ true);
1116
- if (!codecs.length) {
1117
- return null;
1118
- }
1119
- const onlyAudio = hasAudio && !hasVideo;
1120
- return {
1121
- type: onlyAudio ? 'audio' : 'video',
1122
- mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4',
1123
- codecs: this.filterDuplicateCodecs_(codecs).join(', '),
1124
- language: language,
1125
- height: height,
1126
- width: width,
1127
- channelCount: channelCount,
1128
- sampleRate: sampleRate,
1129
- };
1130
- }
1131
-
1132
948
  /** @private */
1133
949
  determineDuration_() {
1134
950
  goog.asserts.assert(this.presentationTimeline_,
@@ -1183,31 +999,50 @@ shaka.hls.HlsParser = class {
1183
999
  * @private
1184
1000
  */
1185
1001
  parseMasterVariables_(tags) {
1002
+ const queryParams = new goog.Uri(this.masterPlaylistUri_).getQueryData();
1186
1003
  for (const variableTag of tags) {
1187
1004
  const name = variableTag.getAttributeValue('NAME');
1188
1005
  const value = variableTag.getAttributeValue('VALUE');
1006
+ const queryParam = variableTag.getAttributeValue('QUERYPARAM');
1189
1007
  if (name && value) {
1190
1008
  if (!this.globalVariables_.has(name)) {
1191
1009
  this.globalVariables_.set(name, value);
1192
1010
  }
1193
1011
  }
1012
+ if (queryParam) {
1013
+ const queryParamValue = queryParams.get(queryParam)[0];
1014
+ if (queryParamValue && !this.globalVariables_.has(queryParamValue)) {
1015
+ this.globalVariables_.set(queryParam, queryParamValue);
1016
+ }
1017
+ }
1194
1018
  }
1195
1019
  }
1196
1020
 
1197
1021
  /**
1198
1022
  * Get the variables of each variant tag, and store in a map.
1199
1023
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
1024
+ * @param {string} uri Media playlist URI.
1200
1025
  * @return {!Map.<string, string>}
1201
1026
  * @private
1202
1027
  */
1203
- parseMediaVariables_(tags) {
1028
+ parseMediaVariables_(tags, uri) {
1029
+ const queryParams = new goog.Uri(uri).getQueryData();
1204
1030
  const mediaVariables = new Map();
1205
1031
  for (const variableTag of tags) {
1206
1032
  const name = variableTag.getAttributeValue('NAME');
1207
1033
  const value = variableTag.getAttributeValue('VALUE');
1034
+ const queryParam = variableTag.getAttributeValue('QUERYPARAM');
1208
1035
  const mediaImport = variableTag.getAttributeValue('IMPORT');
1209
1036
  if (name && value) {
1210
- mediaVariables.set(name, value);
1037
+ if (!mediaVariables.has(name)) {
1038
+ mediaVariables.set(name, value);
1039
+ }
1040
+ }
1041
+ if (queryParam) {
1042
+ const queryParamValue = queryParams.get(queryParam)[0];
1043
+ if (queryParamValue && !mediaVariables.has(queryParamValue)) {
1044
+ mediaVariables.set(queryParam, queryParamValue);
1045
+ }
1211
1046
  }
1212
1047
  if (mediaImport) {
1213
1048
  const globalValue = this.globalVariables_.get(mediaImport);
@@ -1260,6 +1095,68 @@ shaka.hls.HlsParser = class {
1260
1095
  }
1261
1096
  }
1262
1097
 
1098
+ /**
1099
+ * Process EXT-X-SESSION-DATA tags.
1100
+ *
1101
+ * @param {!Array.<!shaka.hls.Tag>} tags
1102
+ * @private
1103
+ */
1104
+ processSessionData_(tags) {
1105
+ for (const tag of tags) {
1106
+ const id = tag.getAttributeValue('DATA-ID');
1107
+ const uri = tag.getAttributeValue('URI');
1108
+ const language = tag.getAttributeValue('LANGUAGE');
1109
+ const value = tag.getAttributeValue('VALUE');
1110
+ const data = (new Map()).set('id', id);
1111
+ if (uri) {
1112
+ data.set('uri', shaka.hls.Utils.constructSegmentUris(
1113
+ [this.masterPlaylistUri_], uri, this.globalVariables_)[0]);
1114
+ }
1115
+ if (language) {
1116
+ data.set('language', language);
1117
+ }
1118
+ if (value) {
1119
+ data.set('value', value);
1120
+ }
1121
+ const event = new shaka.util.FakeEvent('sessiondata', data);
1122
+ if (this.playerInterface_) {
1123
+ this.playerInterface_.onEvent(event);
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Process EXT-X-CONTENT-STEERING tags.
1130
+ *
1131
+ * @param {!Array.<!shaka.hls.Tag>} tags
1132
+ * @return {!Promise}
1133
+ * @private
1134
+ */
1135
+ async processContentSteering_(tags) {
1136
+ if (!this.playerInterface_ || !this.config_) {
1137
+ return;
1138
+ }
1139
+ let contentSteeringPromise;
1140
+ for (const tag of tags) {
1141
+ const defaultPathwayId = tag.getAttributeValue('PATHWAY-ID');
1142
+ const uri = tag.getAttributeValue('SERVER-URI');
1143
+ if (!defaultPathwayId || !uri) {
1144
+ continue;
1145
+ }
1146
+ this.contentSteeringManager_ =
1147
+ new shaka.util.ContentSteeringManager(this.playerInterface_);
1148
+ this.contentSteeringManager_.configure(this.config_);
1149
+ this.contentSteeringManager_.setBaseUris([this.masterPlaylistUri_]);
1150
+ this.contentSteeringManager_.setManifestType(
1151
+ shaka.media.ManifestParser.HLS);
1152
+ this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId);
1153
+ contentSteeringPromise =
1154
+ this.contentSteeringManager_.requestInfo(uri);
1155
+ break;
1156
+ }
1157
+ await contentSteeringPromise;
1158
+ }
1159
+
1263
1160
  /**
1264
1161
  * Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags.
1265
1162
  * Create text streams for Subtitles, but not Closed Captions.
@@ -1278,7 +1175,7 @@ shaka.hls.HlsParser = class {
1278
1175
  return null;
1279
1176
  }
1280
1177
  try {
1281
- return this.createStreamInfoFromMediaTag_(tag).stream;
1178
+ return this.createStreamInfoFromMediaTags_([tag], new Map()).stream;
1282
1179
  } catch (e) {
1283
1180
  if (this.config_.hls.ignoreTextStreamFailures) {
1284
1181
  return null;
@@ -1312,10 +1209,11 @@ shaka.hls.HlsParser = class {
1312
1209
 
1313
1210
  /**
1314
1211
  * @param {!Array.<!shaka.hls.Tag>} imageTags from the playlist.
1212
+ * @param {!Array.<!shaka.hls.Tag>} iFrameTags from the playlist.
1315
1213
  * @return {!Promise.<!Array.<!shaka.extern.Stream>>}
1316
1214
  * @private
1317
1215
  */
1318
- async parseImages_(imageTags) {
1216
+ async parseImages_(imageTags, iFrameTags) {
1319
1217
  // Create image stream for each image tag.
1320
1218
  const imageStreamPromises = imageTags.map(async (tag) => {
1321
1219
  const disableThumbnails = this.config_.disableThumbnails;
@@ -1331,16 +1229,34 @@ shaka.hls.HlsParser = class {
1331
1229
  }
1332
1230
  throw e;
1333
1231
  }
1334
- });
1232
+ }).concat(iFrameTags.map((tag) => {
1233
+ const disableThumbnails = this.config_.disableThumbnails;
1234
+ if (disableThumbnails) {
1235
+ return null;
1236
+ }
1237
+ try {
1238
+ const streamInfo = this.createStreamInfoFromIframeTag_(tag);
1239
+ if (streamInfo.stream.codecs !== 'mjpg') {
1240
+ return null;
1241
+ }
1242
+ return streamInfo.stream;
1243
+ } catch (e) {
1244
+ if (this.config_.hls.ignoreImageStreamFailures) {
1245
+ return null;
1246
+ }
1247
+ throw e;
1248
+ }
1249
+ }));
1335
1250
  const imageStreams = await Promise.all(imageStreamPromises);
1336
1251
  return imageStreams.filter((s) => s);
1337
1252
  }
1338
1253
 
1339
1254
  /**
1340
1255
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
1256
+ * @param {!Map.<string, string>} groupIdPathwayIdMapping
1341
1257
  * @private
1342
1258
  */
1343
- createStreamInfosFromMediaTags_(mediaTags) {
1259
+ createStreamInfosFromMediaTags_(mediaTags, groupIdPathwayIdMapping) {
1344
1260
  // Filter out subtitles and media tags without uri.
1345
1261
  mediaTags = mediaTags.filter((tag) => {
1346
1262
  const uri = tag.getAttributeValue('URI') || '';
@@ -1348,9 +1264,20 @@ shaka.hls.HlsParser = class {
1348
1264
  return type != 'SUBTITLES' && uri != '';
1349
1265
  });
1350
1266
 
1351
- // Create stream info for each audio / video media tag.
1267
+ const groupedTags = {};
1352
1268
  for (const tag of mediaTags) {
1353
- this.createStreamInfoFromMediaTag_(tag);
1269
+ const key = tag.getTagKey();
1270
+ if (!groupedTags[key]) {
1271
+ groupedTags[key] = [tag];
1272
+ } else {
1273
+ groupedTags[key].push(tag);
1274
+ }
1275
+ }
1276
+
1277
+ for (const key in groupedTags) {
1278
+ // Create stream info for each audio / video media grouped tag.
1279
+ this.createStreamInfoFromMediaTags_(
1280
+ groupedTags[key], groupIdPathwayIdMapping);
1354
1281
  }
1355
1282
  }
1356
1283
 
@@ -1358,10 +1285,12 @@ shaka.hls.HlsParser = class {
1358
1285
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
1359
1286
  * @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
1360
1287
  * from the playlist.
1288
+ * @param {!Array.<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
1289
+ * playlist.
1361
1290
  * @return {!Array.<!shaka.extern.Variant>}
1362
1291
  * @private
1363
1292
  */
1364
- createVariantsForTags_(tags, sessionKeyTags) {
1293
+ createVariantsForTags_(tags, sessionKeyTags, mediaTags) {
1365
1294
  // EXT-X-SESSION-KEY processing
1366
1295
  const drmInfos = [];
1367
1296
  const keyIds = new Set();
@@ -1393,24 +1322,54 @@ shaka.hls.HlsParser = class {
1393
1322
  }
1394
1323
  }
1395
1324
 
1396
- // Create variants for each variant tag.
1397
- const allVariants = tags.map((tag) => {
1398
- const frameRate = tag.getAttributeValue('FRAME-RATE');
1399
- const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
1400
- Number(tag.getRequiredAttrValue('BANDWIDTH'));
1325
+ const groupedTags = {};
1326
+ for (const tag of tags) {
1327
+ const key = tag.getTagKey();
1328
+ if (!groupedTags[key]) {
1329
+ groupedTags[key] = [tag];
1330
+ } else {
1331
+ groupedTags[key].push(tag);
1332
+ }
1333
+ }
1334
+
1335
+ const allVariants = [];
1336
+ // Create variants for each group of variant tag.
1337
+ for (const key in groupedTags) {
1338
+ const tags = groupedTags[key];
1339
+ const firstTag = tags[0];
1340
+
1341
+ const frameRate = firstTag.getAttributeValue('FRAME-RATE');
1342
+ const bandwidth =
1343
+ Number(firstTag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
1344
+ Number(firstTag.getRequiredAttrValue('BANDWIDTH'));
1401
1345
 
1402
- const resolution = tag.getAttributeValue('RESOLUTION');
1346
+ const resolution = firstTag.getAttributeValue('RESOLUTION');
1403
1347
  const [width, height] = resolution ? resolution.split('x') : [null, null];
1404
1348
 
1405
- const videoRange = tag.getAttributeValue('VIDEO-RANGE');
1349
+ const videoRange = firstTag.getAttributeValue('VIDEO-RANGE');
1350
+
1351
+ let videoLayout = firstTag.getAttributeValue('REQ-VIDEO-LAYOUT');
1352
+ if (videoLayout && videoLayout.includes(',')) {
1353
+ // If multiple video layout strings are present, pick the first valid
1354
+ // one.
1355
+ const layoutStrings = videoLayout.split(',').filter((layoutString) => {
1356
+ return layoutString == 'CH-STEREO' || layoutString == 'CH-MONO';
1357
+ });
1358
+ videoLayout = layoutStrings[0];
1359
+ }
1360
+ // According to the HLS spec:
1361
+ // By default a video variant is monoscopic, so an attribute
1362
+ // consisting entirely of REQ-VIDEO-LAYOUT="CH-MONO" is unnecessary
1363
+ // and SHOULD NOT be present.
1364
+ videoLayout = videoLayout || 'CH-MONO';
1406
1365
 
1407
- const streamInfos = this.createStreamInfosForVariantTag_(tag,
1408
- resolution, frameRate);
1366
+ const streamInfos = this.createStreamInfosForVariantTags_(tags,
1367
+ mediaTags, resolution, frameRate);
1409
1368
 
1410
1369
  goog.asserts.assert(streamInfos.audio.length ||
1411
1370
  streamInfos.video.length, 'We should have created a stream!');
1412
1371
 
1413
- return this.createVariants_(
1372
+ allVariants.push(...this.createVariants_(
1414
1373
  streamInfos.audio,
1415
1374
  streamInfos.video,
1416
1375
  bandwidth,
@@ -1418,47 +1377,78 @@ shaka.hls.HlsParser = class {
1418
1377
  height,
1419
1378
  frameRate,
1420
1379
  videoRange,
1380
+ videoLayout,
1421
1381
  drmInfos,
1422
- keyIds);
1423
- });
1424
- let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
1425
- // Filter out null variants.
1426
- variants = variants.filter((variant) => variant != null);
1427
- return variants;
1382
+ keyIds));
1383
+ }
1384
+ return allVariants.filter((variant) => variant != null);
1428
1385
  }
1429
1386
 
1430
1387
  /**
1431
1388
  * Create audio and video streamInfos from an 'EXT-X-STREAM-INF' tag and its
1432
1389
  * related media tags.
1433
1390
  *
1434
- * @param {!shaka.hls.Tag} tag
1391
+ * @param {!Array.<!shaka.hls.Tag>} tags
1392
+ * @param {!Array.<!shaka.hls.Tag>} mediaTags
1435
1393
  * @param {?string} resolution
1436
1394
  * @param {?string} frameRate
1437
1395
  * @return {!shaka.hls.HlsParser.StreamInfos}
1438
1396
  * @private
1439
1397
  */
1440
- createStreamInfosForVariantTag_(tag, resolution, frameRate) {
1398
+ createStreamInfosForVariantTags_(tags, mediaTags, resolution, frameRate) {
1441
1399
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1442
- /** @type {!Array.<string>} */
1443
- let allCodecs = this.getCodecsForVariantTag_(tag);
1444
- const audioGroupId = tag.getAttributeValue('AUDIO');
1445
- const videoGroupId = tag.getAttributeValue('VIDEO');
1446
- goog.asserts.assert(audioGroupId == null || videoGroupId == null,
1447
- 'Unexpected: both video and audio described by media tags!');
1448
-
1449
- const groupId = audioGroupId || videoGroupId;
1450
- const streamInfos =
1451
- (groupId && this.groupIdToStreamInfosMap_.has(groupId)) ?
1452
- this.groupIdToStreamInfosMap_.get(groupId) : [];
1453
1400
 
1454
1401
  /** @type {shaka.hls.HlsParser.StreamInfos} */
1455
1402
  const res = {
1456
- audio: audioGroupId ? streamInfos : [],
1457
- video: videoGroupId ? streamInfos : [],
1403
+ audio: [],
1404
+ video: [],
1458
1405
  };
1406
+ const groupIdPathwayIdMapping = new Map();
1407
+ const globalGroupIds = [];
1408
+ let isAudioGroup = false;
1409
+ let isVideoGroup = false;
1410
+ for (const tag of tags) {
1411
+ const audioGroupId = tag.getAttributeValue('AUDIO');
1412
+ const videoGroupId = tag.getAttributeValue('VIDEO');
1413
+ goog.asserts.assert(audioGroupId == null || videoGroupId == null,
1414
+ 'Unexpected: both video and audio described by media tags!');
1415
+
1416
+ const groupId = audioGroupId || videoGroupId;
1417
+ if (!groupId) {
1418
+ continue;
1419
+ }
1420
+ if (!globalGroupIds.includes(groupId)) {
1421
+ globalGroupIds.push(groupId);
1422
+ }
1423
+ const pathwayId = tag.getAttributeValue('PATHWAY-ID');
1424
+ if (pathwayId) {
1425
+ groupIdPathwayIdMapping.set(groupId, pathwayId);
1426
+ }
1427
+ if (audioGroupId) {
1428
+ isAudioGroup = true;
1429
+ } else if (videoGroupId) {
1430
+ isVideoGroup = true;
1431
+ }
1432
+ // Make an educated guess about the stream type.
1433
+ shaka.log.debug('Guessing stream type for', tag.toString());
1434
+ }
1435
+ if (globalGroupIds.length && mediaTags.length) {
1436
+ const mediaTagsForVariant = mediaTags.filter((tag) => {
1437
+ return globalGroupIds.includes(tag.getRequiredAttrValue('GROUP-ID'));
1438
+ });
1439
+ this.createStreamInfosFromMediaTags_(
1440
+ mediaTagsForVariant, groupIdPathwayIdMapping);
1441
+ }
1442
+ const globalGroupId = globalGroupIds.sort().join(',');
1443
+ const streamInfos =
1444
+ (globalGroupId && this.groupIdToStreamInfosMap_.has(globalGroupId)) ?
1445
+ this.groupIdToStreamInfosMap_.get(globalGroupId) : [];
1446
+ if (isAudioGroup) {
1447
+ res.audio.push(...streamInfos);
1448
+ } else if (isVideoGroup) {
1449
+ res.video.push(...streamInfos);
1450
+ }
1459
1451
 
1460
- // Make an educated guess about the stream type.
1461
- shaka.log.debug('Guessing stream type for', tag.toString());
1462
1452
  let type;
1463
1453
  let ignoreStream = false;
1464
1454
 
@@ -1467,11 +1457,15 @@ shaka.hls.HlsParser = class {
1467
1457
  // In that case, ignore the local URI and use the version in the
1468
1458
  // AUDIO tag, so you inherit its language.
1469
1459
  // As an example, see the manifest linked in issue #860.
1470
- const streamURI = tag.getRequiredAttrValue('URI');
1460
+ const allStreamUris = tags.map((tag) => tag.getRequiredAttrValue('URI'));
1471
1461
  const hasSameUri = res.audio.find((audio) => {
1472
- return audio && audio.verbatimMediaPlaylistUri == streamURI;
1462
+ return audio && audio.getUris().find((uri) => {
1463
+ return allStreamUris.includes(uri);
1464
+ });
1473
1465
  });
1474
1466
 
1467
+ /** @type {!Array.<string>} */
1468
+ let allCodecs = this.getCodecsForVariantTag_(tags[0]);
1475
1469
  const videoCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
1476
1470
  ContentType.VIDEO, allCodecs);
1477
1471
  const audioCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
@@ -1483,7 +1477,8 @@ shaka.hls.HlsParser = class {
1483
1477
  type = ContentType.AUDIO;
1484
1478
  shaka.log.debug('Guessing audio-only.');
1485
1479
  ignoreStream = res.audio.length > 0;
1486
- } else if (!streamInfos.length && audioCodecs && videoCodecs) {
1480
+ } else if (!res.audio.length && !res.video.length &&
1481
+ audioCodecs && videoCodecs) {
1487
1482
  // There are both audio and video codecs, so assume multiplexed content.
1488
1483
  // Note that the default used when CODECS is missing assumes multiple
1489
1484
  // (and therefore multiplexed).
@@ -1506,8 +1501,35 @@ shaka.hls.HlsParser = class {
1506
1501
  }
1507
1502
 
1508
1503
  if (!ignoreStream) {
1509
- const streamInfo =
1510
- this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1504
+ let language = null;
1505
+ let name = null;
1506
+ let channelsCount = null;
1507
+ let spatialAudio = false;
1508
+ let characteristics = null;
1509
+ let sampleRate = null;
1510
+ if (!streamInfos.length) {
1511
+ const mediaTag = mediaTags.find((tag) => {
1512
+ const uri = tag.getAttributeValue('URI') || '';
1513
+ const type = tag.getAttributeValue('TYPE');
1514
+ const groupId = tag.getRequiredAttrValue('GROUP-ID');
1515
+ return type != 'SUBTITLES' && uri == '' &&
1516
+ globalGroupIds.includes(groupId);
1517
+ });
1518
+ if (mediaTag) {
1519
+ language = mediaTag.getAttributeValue('LANGUAGE');
1520
+ name = mediaTag.getAttributeValue('NAME');
1521
+ channelsCount = this.getChannelsCount_(mediaTag);
1522
+ spatialAudio = this.isSpatialAudio_(mediaTag);
1523
+ characteristics = mediaTag.getAttributeValue('CHARACTERISTICS');
1524
+ sampleRate = this.getSampleRate_(mediaTag);
1525
+ }
1526
+ }
1527
+ const streamInfo = this.createStreamInfoFromVariantTags_(
1528
+ tags, allCodecs, type, language, name, channelsCount,
1529
+ characteristics, sampleRate, spatialAudio);
1530
+ if (globalGroupId) {
1531
+ streamInfo.stream.groupId = globalGroupId;
1532
+ }
1511
1533
  res[streamInfo.stream.type] = [streamInfo];
1512
1534
  }
1513
1535
  return res;
@@ -1522,17 +1544,24 @@ shaka.hls.HlsParser = class {
1522
1544
  * @private
1523
1545
  */
1524
1546
  getCodecsForVariantTag_(tag) {
1525
- // These are the default codecs to assume if none are specified.
1526
- const defaultCodecsArray = [];
1527
- if (!this.config_.disableVideo) {
1528
- defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
1529
- }
1530
- if (!this.config_.disableAudio) {
1531
- defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
1547
+ let codecsString = tag.getAttributeValue('CODECS') || '';
1548
+
1549
+ this.codecInfoInManifest_ = codecsString.length > 0;
1550
+
1551
+ if (!this.codecInfoInManifest_ && !this.config_.hls.disableCodecGuessing) {
1552
+ // These are the default codecs to assume if none are specified.
1553
+ const defaultCodecsArray = [];
1554
+
1555
+ if (!this.config_.disableVideo) {
1556
+ defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
1557
+ }
1558
+ if (!this.config_.disableAudio) {
1559
+ defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
1560
+ }
1561
+
1562
+ codecsString = defaultCodecsArray.join(',');
1532
1563
  }
1533
- const defaultCodecs = defaultCodecsArray.join(',');
1534
1564
 
1535
- const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
1536
1565
  // Strip out internal whitespace while splitting on commas:
1537
1566
  /** @type {!Array.<string>} */
1538
1567
  const codecs = codecsString.split(/\s*,\s*/);
@@ -1686,6 +1715,7 @@ shaka.hls.HlsParser = class {
1686
1715
  * @param {?string} height
1687
1716
  * @param {?string} frameRate
1688
1717
  * @param {?string} videoRange
1718
+ * @param {?string} videoLayout
1689
1719
  * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1690
1720
  * @param {!Set.<string>} keyIds
1691
1721
  * @return {!Array.<!shaka.extern.Variant>}
@@ -1693,13 +1723,13 @@ shaka.hls.HlsParser = class {
1693
1723
  */
1694
1724
  createVariants_(
1695
1725
  audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1696
- drmInfos, keyIds) {
1726
+ videoLayout, drmInfos, keyIds) {
1697
1727
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1698
1728
  const DrmEngine = shaka.media.DrmEngine;
1699
1729
 
1700
1730
  for (const info of videoInfos) {
1701
1731
  this.addVideoAttributes_(
1702
- info.stream, width, height, frameRate, videoRange);
1732
+ info.stream, width, height, frameRate, videoRange, videoLayout);
1703
1733
  }
1704
1734
 
1705
1735
  // In case of audio-only or video-only content or the audio/video is
@@ -1731,9 +1761,9 @@ shaka.hls.HlsParser = class {
1731
1761
  const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
1732
1762
  const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
1733
1763
  const videoStreamUri =
1734
- videoInfo ? videoInfo.verbatimMediaPlaylistUri : '';
1764
+ videoInfo ? videoInfo.getUris().sort().join(',') : '';
1735
1765
  const audioStreamUri =
1736
- audioInfo ? audioInfo.verbatimMediaPlaylistUri : '';
1766
+ audioInfo ? audioInfo.getUris().sort().join(',') : '';
1737
1767
  const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
1738
1768
 
1739
1769
  if (audioStream && videoStream) {
@@ -1821,34 +1851,59 @@ shaka.hls.HlsParser = class {
1821
1851
  /**
1822
1852
  * Parse EXT-X-MEDIA media tag into a Stream object.
1823
1853
  *
1824
- * @param {shaka.hls.Tag} tag
1854
+ * @param {!Array.<!shaka.hls.Tag>} tags
1855
+ * @param {!Map.<string, string>} groupIdPathwayIdMapping
1825
1856
  * @return {!shaka.hls.HlsParser.StreamInfo}
1826
1857
  * @private
1827
1858
  */
1828
- createStreamInfoFromMediaTag_(tag) {
1829
- goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1830
- 'Should only be called on media tags!');
1831
- const groupId = tag.getRequiredAttrValue('GROUP-ID');
1859
+ createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping) {
1860
+ const verbatimMediaPlaylistUris = [];
1861
+ const globalGroupIds = [];
1862
+ const groupIdUriMappping = new Map();
1863
+ for (const tag of tags) {
1864
+ goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1865
+ 'Should only be called on media tags!');
1866
+ const uri = tag.getRequiredAttrValue('URI');
1867
+ const groupId = tag.getRequiredAttrValue('GROUP-ID');
1868
+ verbatimMediaPlaylistUris.push(uri);
1869
+ globalGroupIds.push(groupId);
1870
+ groupIdUriMappping.set(groupId, uri);
1871
+ }
1872
+
1873
+ const globalGroupId = globalGroupIds.sort().join(',');
1874
+ const firstTag = tags[0];
1832
1875
  let codecs = '';
1833
1876
  /** @type {string} */
1834
- const type = this.getType_(tag);
1877
+ const type = this.getType_(firstTag);
1835
1878
  if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
1836
- codecs = tag.getAttributeValue('CODECS') || '';
1837
- } else if (groupId && this.groupIdToCodecsMap_.has(groupId)) {
1838
- codecs = this.groupIdToCodecsMap_.get(groupId);
1879
+ codecs = firstTag.getAttributeValue('CODECS') || '';
1880
+ } else {
1881
+ for (const groupId of globalGroupIds) {
1882
+ if (this.groupIdToCodecsMap_.has(groupId)) {
1883
+ codecs = this.groupIdToCodecsMap_.get(groupId);
1884
+ break;
1885
+ }
1886
+ }
1839
1887
  }
1840
1888
 
1841
- const verbatimMediaPlaylistUri = this.variableSubstitution_(
1842
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
1843
-
1844
1889
  // Check if the stream has already been created as part of another Variant
1845
1890
  // and return it if it has.
1846
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1847
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1891
+ const key = verbatimMediaPlaylistUris.sort().join(',');
1892
+ if (this.uriToStreamInfosMap_.has(key)) {
1893
+ return this.uriToStreamInfosMap_.get(key);
1894
+ }
1895
+ const streamId = this.globalId_++;
1896
+ if (this.contentSteeringManager_) {
1897
+ for (const [groupId, uri] of groupIdUriMappping) {
1898
+ const pathwayId = groupIdPathwayIdMapping.get(groupId);
1899
+ if (pathwayId) {
1900
+ this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
1901
+ }
1902
+ }
1848
1903
  }
1849
1904
 
1850
- const language = tag.getAttributeValue('LANGUAGE');
1851
- const name = tag.getAttributeValue('NAME');
1905
+ const language = firstTag.getAttributeValue('LANGUAGE');
1906
+ const name = firstTag.getAttributeValue('NAME');
1852
1907
 
1853
1908
  // NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
1854
1909
  // However, we don't bother to validate "AUTOSELECT", since we don't
@@ -1856,39 +1911,39 @@ shaka.hls.HlsParser = class {
1856
1911
  // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
1857
1912
  // only be selected explicitly by the user, and we don't have a way to
1858
1913
  // represent that in our model.
1859
- const defaultAttrValue = tag.getAttributeValue('DEFAULT');
1914
+ const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
1860
1915
  const primary = defaultAttrValue == 'YES';
1861
1916
 
1862
- const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
1863
- const spatialAudio = type == 'audio' ? this.isSpatialAudio_(tag) : false;
1864
- const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1917
+ const channelsCount =
1918
+ type == 'audio' ? this.getChannelsCount_(firstTag) : null;
1919
+ const spatialAudio =
1920
+ type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
1921
+ const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
1865
1922
 
1866
- const forcedAttrValue = tag.getAttributeValue('FORCED');
1923
+ const forcedAttrValue = firstTag.getAttributeValue('FORCED');
1867
1924
  const forced = forcedAttrValue == 'YES';
1868
- const sampleRate = type == 'audio' ? this.getSampleRate_(tag) : null;
1925
+ const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
1869
1926
  // TODO: Should we take into account some of the currently ignored
1870
1927
  // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
1871
1928
  const streamInfo = this.createStreamInfo_(
1872
- verbatimMediaPlaylistUri, codecs, type, language, primary, name,
1873
- channelsCount, /* closedCaptions= */ null, characteristics, forced,
1874
- sampleRate, spatialAudio);
1875
- if (this.groupIdToStreamInfosMap_.has(groupId)) {
1876
- this.groupIdToStreamInfosMap_.get(groupId).push(streamInfo);
1929
+ streamId, verbatimMediaPlaylistUris, codecs, type, language,
1930
+ primary, name, channelsCount, /* closedCaptions= */ null,
1931
+ characteristics, forced, sampleRate, spatialAudio);
1932
+ if (streamInfo.stream) {
1933
+ streamInfo.stream.groupId = globalGroupId;
1934
+ }
1935
+ if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
1936
+ this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
1877
1937
  } else {
1878
- this.groupIdToStreamInfosMap_.set(groupId, [streamInfo]);
1938
+ this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
1879
1939
  }
1880
1940
 
1881
- // TODO: This check is necessary because of the possibility of multiple
1882
- // calls to createStreamInfoFromMediaTag_ before either has resolved.
1883
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1884
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1885
- }
1886
- this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
1941
+ this.uriToStreamInfosMap_.set(key, streamInfo);
1887
1942
  return streamInfo;
1888
1943
  }
1889
1944
 
1890
1945
  /**
1891
- * Parse EXT-X-MEDIA media tag into a Stream object.
1946
+ * Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
1892
1947
  *
1893
1948
  * @param {shaka.hls.Tag} tag
1894
1949
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
@@ -1900,8 +1955,7 @@ shaka.hls.HlsParser = class {
1900
1955
  /** @type {string} */
1901
1956
  const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
1902
1957
 
1903
- const verbatimImagePlaylistUri = this.variableSubstitution_(
1904
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
1958
+ const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
1905
1959
  const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
1906
1960
 
1907
1961
  // Check if the stream has already been created as part of another Variant
@@ -1916,16 +1970,10 @@ shaka.hls.HlsParser = class {
1916
1970
  const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1917
1971
 
1918
1972
  const streamInfo = this.createStreamInfo_(
1919
- verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
1920
- name, /* channelsCount= */ null, /* closedCaptions= */ null,
1921
- characteristics, /* forced= */ false, /* sampleRate= */ null,
1922
- /* spatialAudio= */ false);
1923
-
1924
- // TODO: This check is necessary because of the possibility of multiple
1925
- // calls to createStreamInfoFromImageTag_ before either has resolved.
1926
- if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
1927
- return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
1928
- }
1973
+ this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
1974
+ /* primary= */ false, name, /* channelsCount= */ null,
1975
+ /* closedCaptions= */ null, characteristics, /* forced= */ false,
1976
+ /* sampleRate= */ null, /* spatialAudio= */ false);
1929
1977
 
1930
1978
  // Parse misc attributes.
1931
1979
  const resolution = tag.getAttributeValue('RESOLUTION');
@@ -1958,45 +2006,104 @@ shaka.hls.HlsParser = class {
1958
2006
  return streamInfo;
1959
2007
  }
1960
2008
 
2009
+ /**
2010
+ * Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
2011
+ *
2012
+ * @param {shaka.hls.Tag} tag
2013
+ * @return {!shaka.hls.HlsParser.StreamInfo}
2014
+ * @private
2015
+ */
2016
+ createStreamInfoFromIframeTag_(tag) {
2017
+ goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
2018
+ 'Should only be called on iframe tags!');
2019
+ /** @type {string} */
2020
+ const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
2021
+
2022
+ const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
2023
+ const codecs = tag.getAttributeValue('CODECS') || '';
2024
+
2025
+ // Check if the stream has already been created as part of another Variant
2026
+ // and return it if it has.
2027
+ if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
2028
+ return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
2029
+ }
2030
+
2031
+ const language = tag.getAttributeValue('LANGUAGE');
2032
+ const name = tag.getAttributeValue('NAME');
2033
+
2034
+ const characteristics = tag.getAttributeValue('CHARACTERISTICS');
2035
+
2036
+ const streamInfo = this.createStreamInfo_(
2037
+ this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
2038
+ /* primary= */ false, name, /* channelsCount= */ null,
2039
+ /* closedCaptions= */ null, characteristics, /* forced= */ false,
2040
+ /* sampleRate= */ null, /* spatialAudio= */ false);
2041
+
2042
+ // Parse misc attributes.
2043
+ const resolution = tag.getAttributeValue('RESOLUTION');
2044
+ const [width, height] = resolution ? resolution.split('x') : [null, null];
2045
+ streamInfo.stream.width = Number(width) || undefined;
2046
+ streamInfo.stream.height = Number(height) || undefined;
2047
+ const bandwidth = tag.getAttributeValue('BANDWIDTH');
2048
+ if (bandwidth) {
2049
+ streamInfo.stream.bandwidth = Number(bandwidth);
2050
+ }
2051
+
2052
+ this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
2053
+ return streamInfo;
2054
+ }
2055
+
1961
2056
  /**
1962
2057
  * Parse an EXT-X-STREAM-INF media tag into a Stream object.
1963
2058
  *
1964
- * @param {!shaka.hls.Tag} tag
2059
+ * @param {!Array.<!shaka.hls.Tag>} tags
1965
2060
  * @param {!Array.<string>} allCodecs
1966
2061
  * @param {string} type
2062
+ * @param {?string} language
2063
+ * @param {?string} name
2064
+ * @param {?number} channelsCount
2065
+ * @param {?string} characteristics
2066
+ * @param {?number} sampleRate
2067
+ * @param {boolean} spatialAudio
1967
2068
  * @return {!shaka.hls.HlsParser.StreamInfo}
1968
2069
  * @private
1969
2070
  */
1970
- createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1971
- goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
1972
- 'Should only be called on variant tags!');
1973
- const verbatimMediaPlaylistUri = this.variableSubstitution_(
1974
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
2071
+ createStreamInfoFromVariantTags_(tags, allCodecs, type, language, name,
2072
+ channelsCount, characteristics, sampleRate, spatialAudio) {
2073
+ const streamId = this.globalId_++;
2074
+ const verbatimMediaPlaylistUris = [];
2075
+ for (const tag of tags) {
2076
+ goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
2077
+ 'Should only be called on variant tags!');
2078
+ const uri = tag.getRequiredAttrValue('URI');
2079
+ const pathwayId = tag.getAttributeValue('PATHWAY-ID');
2080
+ if (this.contentSteeringManager_ && pathwayId) {
2081
+ this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
2082
+ }
2083
+ verbatimMediaPlaylistUris.push(uri);
2084
+ }
1975
2085
 
1976
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1977
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
2086
+ const key = verbatimMediaPlaylistUris.sort().join(',');
2087
+ if (this.uriToStreamInfosMap_.has(key)) {
2088
+ return this.uriToStreamInfosMap_.get(key);
1978
2089
  }
1979
2090
 
1980
- const closedCaptions = this.getClosedCaptions_(tag, type);
2091
+ const closedCaptions = this.getClosedCaptions_(tags[0], type);
1981
2092
  const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
1982
- const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
1983
- codecs, type, /* language= */ null, /* primary= */ false,
1984
- /* name= */ null, /* channelcount= */ null, closedCaptions,
1985
- /* characteristics= */ null, /* forced= */ false,
1986
- /* sampleRate= */ null, /* spatialAudio= */ false);
1987
- // TODO: This check is necessary because of the possibility of multiple
1988
- // calls to createStreamInfoFromVariantTag_ before either has resolved.
1989
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1990
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1991
- }
2093
+ const streamInfo = this.createStreamInfo_(
2094
+ streamId, verbatimMediaPlaylistUris, codecs, type, language,
2095
+ /* primary= */ false, name, channelsCount, closedCaptions,
2096
+ characteristics, /* forced= */ false, sampleRate,
2097
+ /* spatialAudio= */ false);
1992
2098
 
1993
- this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
2099
+ this.uriToStreamInfosMap_.set(key, streamInfo);
1994
2100
  return streamInfo;
1995
2101
  }
1996
2102
 
1997
2103
 
1998
2104
  /**
1999
- * @param {string} verbatimMediaPlaylistUri
2105
+ * @param {number} streamId
2106
+ * @param {!Array.<string>} verbatimMediaPlaylistUris
2000
2107
  * @param {string} codecs
2001
2108
  * @param {string} type
2002
2109
  * @param {?string} languageValue
@@ -2011,25 +2118,34 @@ shaka.hls.HlsParser = class {
2011
2118
  * @return {!shaka.hls.HlsParser.StreamInfo}
2012
2119
  * @private
2013
2120
  */
2014
- createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, languageValue,
2015
- primary, name, channelsCount, closedCaptions, characteristics, forced,
2016
- sampleRate, spatialAudio) {
2121
+ createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
2122
+ languageValue, primary, name, channelsCount, closedCaptions,
2123
+ characteristics, forced, sampleRate, spatialAudio) {
2017
2124
  // TODO: Refactor, too many parameters
2018
- const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
2019
- this.masterPlaylistUri_, verbatimMediaPlaylistUri);
2020
2125
 
2021
2126
  // This stream is lazy-loaded inside the createSegmentIndex function.
2022
2127
  // So we start out with a stream object that does not contain the actual
2023
2128
  // segment index, then download when createSegmentIndex is called.
2024
- const stream = this.makeStreamObject_(codecs, type, languageValue, primary,
2025
- name, channelsCount, closedCaptions, characteristics, forced,
2026
- sampleRate, spatialAudio);
2129
+ const stream = this.makeStreamObject_(streamId, codecs, type,
2130
+ languageValue, primary, name, channelsCount, closedCaptions,
2131
+ characteristics, forced, sampleRate, spatialAudio);
2132
+
2133
+ const redirectUris = [];
2134
+ const getUris = () => {
2135
+ if (this.contentSteeringManager_ &&
2136
+ verbatimMediaPlaylistUris.length > 1) {
2137
+ return this.contentSteeringManager_.getLocations(streamId);
2138
+ }
2139
+ return redirectUris.concat(shaka.hls.Utils.constructUris(
2140
+ [this.masterPlaylistUri_], verbatimMediaPlaylistUris,
2141
+ this.globalVariables_));
2142
+ };
2027
2143
  const streamInfo = {
2028
2144
  stream,
2029
2145
  type,
2030
- verbatimMediaPlaylistUri,
2146
+ redirectUris,
2147
+ getUris,
2031
2148
  // These values are filled out or updated after lazy-loading:
2032
- absoluteMediaPlaylistUri: initialMediaPlaylistUri,
2033
2149
  minTimestamp: 0,
2034
2150
  maxTimestamp: 0,
2035
2151
  mediaSequenceToStartTime: new Map(),
@@ -2044,27 +2160,45 @@ shaka.hls.HlsParser = class {
2044
2160
 
2045
2161
  /** @param {!AbortSignal} abortSignal */
2046
2162
  const downloadSegmentIndex = async (abortSignal) => {
2163
+ const uris = streamInfo.getUris();
2047
2164
  // Download the actual manifest.
2048
2165
  const response = await this.requestManifest_(
2049
- streamInfo.absoluteMediaPlaylistUri, /* isPlaylist= */ true);
2166
+ streamInfo.getUris(), /* isPlaylist= */ true);
2050
2167
  if (abortSignal.aborted) {
2051
2168
  return;
2052
2169
  }
2053
2170
 
2054
2171
  // Record the final URI after redirects.
2055
- const absoluteMediaPlaylistUri = response.uri;
2172
+ const responseUri = response.uri;
2173
+ if (responseUri != response.originalUri && !uris.includes(responseUri)) {
2174
+ redirectUris.push(responseUri);
2175
+ }
2056
2176
 
2057
2177
  // Record the redirected, final URI of this media playlist when we parse
2058
2178
  // it.
2059
2179
  /** @type {!shaka.hls.Playlist} */
2060
- const playlist = this.manifestTextParser_.parsePlaylist(
2061
- response.data, absoluteMediaPlaylistUri);
2180
+ const playlist = this.manifestTextParser_.parsePlaylist(response.data);
2181
+
2182
+ let mimeType = undefined;
2183
+
2184
+ // If no codec info was provided in the manifest and codec guessing is
2185
+ // disabled we try to get necessary info from the media data.
2186
+ if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
2187
+ const basicInfo =
2188
+ await this.getMediaPlaylistBasicInfo_(playlist, getUris);
2189
+
2190
+ goog.asserts.assert(
2191
+ type === basicInfo.type, 'Media types should match!');
2192
+
2193
+ mimeType = basicInfo.mimeType;
2194
+ codecs = basicInfo.codecs;
2195
+ }
2062
2196
 
2063
2197
  const wasLive = this.isLive_();
2064
2198
  const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
2065
- playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
2199
+ streamId, playlist, getUris, responseUri, codecs,
2066
2200
  type, languageValue, primary, name, channelsCount, closedCaptions,
2067
- characteristics, forced, sampleRate, spatialAudio);
2201
+ characteristics, forced, sampleRate, spatialAudio, mimeType);
2068
2202
  if (abortSignal.aborted) {
2069
2203
  return;
2070
2204
  }
@@ -2078,7 +2212,6 @@ shaka.hls.HlsParser = class {
2078
2212
  }
2079
2213
 
2080
2214
  // Copy values from the real stream info to our initial one.
2081
- streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
2082
2215
  streamInfo.minTimestamp = realStreamInfo.minTimestamp;
2083
2216
  streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
2084
2217
  streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
@@ -2095,6 +2228,7 @@ shaka.hls.HlsParser = class {
2095
2228
  stream.keyIds = realStream.keyIds;
2096
2229
  stream.mimeType = realStream.mimeType;
2097
2230
  stream.bandwidth = realStream.bandwidth;
2231
+ stream.codecs = realStream.codecs || stream.codecs;
2098
2232
 
2099
2233
  // Since we lazy-loaded this content, the player may need to create new
2100
2234
  // sessions for the DRM info in this stream.
@@ -2296,9 +2430,10 @@ shaka.hls.HlsParser = class {
2296
2430
  }
2297
2431
 
2298
2432
  /**
2433
+ * @param {number} streamId
2299
2434
  * @param {!shaka.hls.Playlist} playlist
2300
- * @param {string} verbatimMediaPlaylistUri
2301
- * @param {string} absoluteMediaPlaylistUri
2435
+ * @param {function():!Array.<string>} getUris
2436
+ * @param {string} responseUri
2302
2437
  * @param {string} codecs
2303
2438
  * @param {string} type
2304
2439
  * @param {?string} languageValue
@@ -2314,8 +2449,8 @@ shaka.hls.HlsParser = class {
2314
2449
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
2315
2450
  * @private
2316
2451
  */
2317
- async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri,
2318
- absoluteMediaPlaylistUri, codecs, type, languageValue, primary, name,
2452
+ async convertParsedPlaylistIntoStreamInfo_(streamId, playlist,
2453
+ getUris, responseUri, codecs, type, languageValue, primary, name,
2319
2454
  channelsCount, closedCaptions, characteristics, forced, sampleRate,
2320
2455
  spatialAudio, mimeType = undefined) {
2321
2456
  if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
@@ -2331,7 +2466,8 @@ shaka.hls.HlsParser = class {
2331
2466
  const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
2332
2467
  'EXT-X-DEFINE');
2333
2468
 
2334
- const mediaVariables = this.parseMediaVariables_(variablesTags);
2469
+ const mediaVariables =
2470
+ this.parseMediaVariables_(variablesTags, responseUri);
2335
2471
 
2336
2472
  goog.asserts.assert(playlist.segments != null,
2337
2473
  'Media playlist should have segments!');
@@ -2344,7 +2480,7 @@ shaka.hls.HlsParser = class {
2344
2480
 
2345
2481
  if (!mimeType) {
2346
2482
  mimeType = await this.guessMimeType_(type, codecs, playlist,
2347
- mediaVariables);
2483
+ mediaVariables, getUris);
2348
2484
  }
2349
2485
 
2350
2486
  const {drmInfos, keyIds, encrypted, aesEncrypted} =
@@ -2357,11 +2493,20 @@ shaka.hls.HlsParser = class {
2357
2493
  shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
2358
2494
  }
2359
2495
 
2496
+ const stream = this.makeStreamObject_(streamId, codecs, type,
2497
+ languageValue, primary, name, channelsCount, closedCaptions,
2498
+ characteristics, forced, sampleRate, spatialAudio);
2499
+ stream.encrypted = encrypted;
2500
+ stream.drmInfos = drmInfos;
2501
+ stream.keyIds = keyIds;
2502
+ stream.mimeType = mimeType;
2503
+
2360
2504
  const mediaSequenceToStartTime = this.isLive_() ?
2361
2505
  this.mediaSequenceToStartTimeByType_.get(type) : new Map();
2506
+
2362
2507
  const {segments, bandwidth} = this.createSegments_(
2363
- verbatimMediaPlaylistUri, playlist, type, mimeType,
2364
- mediaSequenceToStartTime, mediaVariables);
2508
+ playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
2509
+ stream.bandwidth = bandwidth;
2365
2510
 
2366
2511
  // This new calculation is necessary for Low Latency streams.
2367
2512
  if (this.isLive_()) {
@@ -2373,6 +2518,7 @@ shaka.hls.HlsParser = class {
2373
2518
  const lastEndTime = lastSegment.endTime;
2374
2519
  /** @type {!shaka.media.SegmentIndex} */
2375
2520
  const segmentIndex = new shaka.media.SegmentIndex(segments);
2521
+ stream.segmentIndex = segmentIndex;
2376
2522
 
2377
2523
  const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
2378
2524
  playlist.tags, 'EXT-X-SERVER-CONTROL');
@@ -2387,21 +2533,11 @@ shaka.hls.HlsParser = class {
2387
2533
  const {nextMediaSequence, nextPart} =
2388
2534
  this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
2389
2535
 
2390
- const stream = this.makeStreamObject_(codecs, type, languageValue, primary,
2391
- name, channelsCount, closedCaptions, characteristics, forced,
2392
- sampleRate, spatialAudio);
2393
- stream.segmentIndex = segmentIndex;
2394
- stream.encrypted = encrypted;
2395
- stream.drmInfos = drmInfos;
2396
- stream.keyIds = keyIds;
2397
- stream.mimeType = mimeType;
2398
- stream.bandwidth = bandwidth;
2399
-
2400
2536
  return {
2401
2537
  stream,
2402
2538
  type,
2403
- verbatimMediaPlaylistUri,
2404
- absoluteMediaPlaylistUri,
2539
+ redirectUris: [],
2540
+ getUris,
2405
2541
  minTimestamp: firstStartTime,
2406
2542
  maxTimestamp: lastEndTime,
2407
2543
  canSkipSegments,
@@ -2465,6 +2601,7 @@ shaka.hls.HlsParser = class {
2465
2601
  * The parameters that are passed into here are only the things that can be
2466
2602
  * known without downloading the media playlist; other values must be set
2467
2603
  * manually on the object after creation.
2604
+ * @param {number} id
2468
2605
  * @param {string} codecs
2469
2606
  * @param {string} type
2470
2607
  * @param {?string} languageValue
@@ -2479,8 +2616,9 @@ shaka.hls.HlsParser = class {
2479
2616
  * @return {!shaka.extern.Stream}
2480
2617
  * @private
2481
2618
  */
2482
- makeStreamObject_(codecs, type, languageValue, primary, name, channelsCount,
2483
- closedCaptions, characteristics, forced, sampleRate, spatialAudio) {
2619
+ makeStreamObject_(id, codecs, type, languageValue, primary, name,
2620
+ channelsCount, closedCaptions, characteristics, forced, sampleRate,
2621
+ spatialAudio) {
2484
2622
  // Fill out a "best-guess" mimeType, for now. It will be replaced once the
2485
2623
  // stream is lazy-loaded.
2486
2624
  const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
@@ -2492,8 +2630,21 @@ shaka.hls.HlsParser = class {
2492
2630
  }
2493
2631
  }
2494
2632
 
2495
- const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2496
- shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
2633
+ let kind = undefined;
2634
+ let accessibilityPurpose = null;
2635
+ if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2636
+ if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
2637
+ roles.includes('public.accessibility.describes-music-and-sound')) {
2638
+ kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
2639
+ } else {
2640
+ kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
2641
+ }
2642
+ } else {
2643
+ if (roles.includes('public.accessibility.describes-video')) {
2644
+ accessibilityPurpose =
2645
+ shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
2646
+ }
2647
+ }
2497
2648
 
2498
2649
  // If there are no roles, and we have defaulted to the subtitle "kind" for
2499
2650
  // this track, add the implied subtitle role.
@@ -2535,9 +2686,11 @@ shaka.hls.HlsParser = class {
2535
2686
  spatialAudio,
2536
2687
  closedCaptions,
2537
2688
  hdr: undefined,
2689
+ videoLayout: undefined,
2538
2690
  tilesLayout: undefined,
2539
- accessibilityPurpose: null,
2691
+ accessibilityPurpose: accessibilityPurpose,
2540
2692
  external: false,
2693
+ fastSwitching: false,
2541
2694
  };
2542
2695
  }
2543
2696
 
@@ -2608,10 +2761,12 @@ shaka.hls.HlsParser = class {
2608
2761
  /**
2609
2762
  * @param {!shaka.hls.Tag} drmTag
2610
2763
  * @param {!shaka.hls.Playlist} playlist
2764
+ * @param {function():!Array.<string>} getUris
2765
+ * @param {?Map.<string, string>=} variables
2611
2766
  * @return {!shaka.extern.aes128Key}
2612
2767
  * @private
2613
2768
  */
2614
- parseAES128DrmTag_(drmTag, playlist) {
2769
+ parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
2615
2770
  // Check if the Web Crypto API is available.
2616
2771
  if (!window.crypto || !window.crypto.subtle) {
2617
2772
  shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
@@ -2646,18 +2801,17 @@ shaka.hls.HlsParser = class {
2646
2801
  }
2647
2802
  }
2648
2803
 
2649
- const keyUri = shaka.hls.Utils.constructAbsoluteUri(
2650
- playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
2651
-
2652
- const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2653
- const request = shaka.net.NetworkingEngine.makeRequest(
2654
- [keyUri], this.config_.retryParameters);
2655
-
2656
2804
  const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
2657
2805
 
2658
2806
  // Don't download the key object until the segment is parsed, to avoid a
2659
2807
  // startup delay for long manifests with lots of keys.
2660
2808
  keyInfo.fetchKey = async () => {
2809
+ const keyUri = shaka.hls.Utils.constructSegmentUris(
2810
+ getUris(), drmTag.getRequiredAttrValue('URI'), variables);
2811
+
2812
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2813
+ const request = shaka.net.NetworkingEngine.makeRequest(
2814
+ [keyUri], this.config_.retryParameters);
2661
2815
  const keyResponse = await this.makeNetworkRequest_(request, requestType);
2662
2816
 
2663
2817
  // keyResponse.status is undefined when URI is "data:text/plain;base64,"
@@ -2834,11 +2988,12 @@ shaka.hls.HlsParser = class {
2834
2988
  * Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
2835
2989
  * @param {!shaka.hls.Playlist} playlist
2836
2990
  * @param {!Array.<!shaka.hls.Tag>} tags Segment tags
2837
- * @param {!Map.<string, string>} variables
2991
+ * @param {function():!Array.<string>} getUris
2992
+ * @param {?Map.<string, string>=} variables
2838
2993
  * @return {shaka.media.InitSegmentReference}
2839
2994
  * @private
2840
2995
  */
2841
- getInitSegmentReference_(playlist, tags, variables) {
2996
+ getInitSegmentReference_(playlist, tags, getUris, variables) {
2842
2997
  /** @type {?shaka.hls.Tag} */
2843
2998
  const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
2844
2999
 
@@ -2847,28 +3002,30 @@ shaka.hls.HlsParser = class {
2847
3002
  }
2848
3003
  // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
2849
3004
  const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
2850
- const absoluteInitSegmentUri = this.variableSubstitution_(
2851
- shaka.hls.Utils.constructAbsoluteUri(
2852
- playlist.absoluteUri, verbatimInitSegmentUri),
2853
- variables);
3005
+ const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
3006
+ getUris(), verbatimInitSegmentUri, variables);
2854
3007
 
2855
3008
  const mapTagKey = [
2856
- absoluteInitSegmentUri,
3009
+ absoluteInitSegmentUris.toString(),
2857
3010
  mapTag.getAttributeValue('BYTERANGE', ''),
2858
3011
  ].join('-');
2859
3012
  if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
2860
3013
  /** @type {shaka.extern.aes128Key|undefined} */
2861
3014
  let aes128Key = undefined;
3015
+ let byteRangeTag = null;
2862
3016
  for (const tag of tags) {
2863
3017
  if (tag.name == 'EXT-X-KEY') {
2864
3018
  if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
2865
3019
  tag.id < mapTag.id) {
2866
- aes128Key = this.parseAES128DrmTag_(tag, playlist);
3020
+ aes128Key =
3021
+ this.parseAES128DrmTag_(tag, playlist, getUris, variables);
2867
3022
  }
3023
+ } else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
3024
+ byteRangeTag = tag;
2868
3025
  }
2869
3026
  }
2870
3027
  const initSegmentRef = this.createInitSegmentReference_(
2871
- absoluteInitSegmentUri, mapTag, aes128Key);
3028
+ absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
2872
3029
  this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
2873
3030
  }
2874
3031
  return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
@@ -2877,16 +3034,21 @@ shaka.hls.HlsParser = class {
2877
3034
  /**
2878
3035
  * Create an InitSegmentReference object for the EXT-X-MAP tag in the media
2879
3036
  * playlist.
2880
- * @param {string} absoluteInitSegmentUri
3037
+ * @param {!Array.<string>} absoluteInitSegmentUris
2881
3038
  * @param {!shaka.hls.Tag} mapTag EXT-X-MAP
3039
+ * @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
2882
3040
  * @param {shaka.extern.aes128Key=} aes128Key
2883
3041
  * @return {!shaka.media.InitSegmentReference}
2884
3042
  * @private
2885
3043
  */
2886
- createInitSegmentReference_(absoluteInitSegmentUri, mapTag, aes128Key) {
3044
+ createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
3045
+ aes128Key) {
2887
3046
  let startByte = 0;
2888
3047
  let endByte = null;
2889
- const byterange = mapTag.getAttributeValue('BYTERANGE');
3048
+ let byterange = mapTag.getAttributeValue('BYTERANGE');
3049
+ if (!byterange && byteRangeTag) {
3050
+ byterange = byteRangeTag.value;
3051
+ }
2890
3052
  // If a BYTERANGE attribute is not specified, the segment consists
2891
3053
  // of the entire resource.
2892
3054
  if (byterange) {
@@ -2907,7 +3069,7 @@ shaka.hls.HlsParser = class {
2907
3069
  }
2908
3070
 
2909
3071
  const initSegmentRef = new shaka.media.InitSegmentReference(
2910
- () => [absoluteInitSegmentUri],
3072
+ () => absoluteInitSegmentUris,
2911
3073
  startByte,
2912
3074
  endByte,
2913
3075
  /* mediaQuality= */ null,
@@ -2925,16 +3087,16 @@ shaka.hls.HlsParser = class {
2925
3087
  * @param {!shaka.hls.Segment} hlsSegment
2926
3088
  * @param {number} startTime
2927
3089
  * @param {!Map.<string, string>} variables
2928
- * @param {string} absoluteMediaPlaylistUri
2929
- * @param {string} type
2930
- * @param {string} mimeType
3090
+ * @param {!shaka.hls.Playlist} playlist
3091
+ * @param {shaka.extern.Stream} stream
3092
+ * @param {function():!Array.<string>} getUris
2931
3093
  * @param {shaka.extern.aes128Key=} aes128Key
2932
3094
  * @return {shaka.media.SegmentReference}
2933
3095
  * @private
2934
3096
  */
2935
3097
  createSegmentReference_(
2936
3098
  initSegmentReference, previousReference, hlsSegment, startTime,
2937
- variables, absoluteMediaPlaylistUri, type, mimeType, aes128Key) {
3099
+ variables, playlist, stream, getUris, aes128Key) {
2938
3100
  const tags = hlsSegment.tags;
2939
3101
  const extinfTag =
2940
3102
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
@@ -2991,8 +3153,9 @@ shaka.hls.HlsParser = class {
2991
3153
  let isPreloadSegment = false;
2992
3154
 
2993
3155
  if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
2994
- const byterangeOptimizationSupport = (mimeType == 'video/mp4' ||
2995
- mimeType == 'audio/mp4') && window.ReadableStream;
3156
+ const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' ||
3157
+ stream.mimeType == 'audio/mp4') && window.ReadableStream &&
3158
+ this.config_.hls.allowLowLatencyByteRangeOptimization;
2996
3159
 
2997
3160
  let partialSyncTime = syncTime;
2998
3161
  for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
@@ -3048,14 +3211,14 @@ shaka.hls.HlsParser = class {
3048
3211
  somePartialSegmentWithGap = true;
3049
3212
  }
3050
3213
 
3051
- let pAbsoluteUri = null;
3214
+ let uris = null;
3052
3215
  const getPartialUris = () => {
3053
- if (pAbsoluteUri == null) {
3216
+ if (uris == null) {
3054
3217
  goog.asserts.assert(pUri, 'Partial uri should be defined!');
3055
- pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri(
3056
- absoluteMediaPlaylistUri, pUri);
3218
+ uris = shaka.hls.Utils.constructSegmentUris(
3219
+ getUris(), pUri, variables);
3057
3220
  }
3058
- return [pAbsoluteUri];
3221
+ return uris;
3059
3222
  };
3060
3223
 
3061
3224
  if (byterangeOptimizationSupport &&
@@ -3150,7 +3313,7 @@ shaka.hls.HlsParser = class {
3150
3313
 
3151
3314
  let tilesLayout = '';
3152
3315
  let tileDuration = null;
3153
- if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
3316
+ if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
3154
3317
  // By default in HLS the tilesLayout is 1x1
3155
3318
  tilesLayout = '1x1';
3156
3319
  const tilesTag =
@@ -3164,22 +3327,25 @@ shaka.hls.HlsParser = class {
3164
3327
  }
3165
3328
  }
3166
3329
 
3167
- let absoluteSegmentUri = null;
3168
- const getUris = () => {
3330
+ let uris = null;
3331
+ const getSegmentUris = () => {
3169
3332
  if (getUrisOptimization) {
3170
3333
  return getUrisOptimization();
3171
3334
  }
3172
- if (absoluteSegmentUri == null) {
3173
- absoluteSegmentUri = this.variableSubstitution_(
3174
- hlsSegment.absoluteUri, variables);
3335
+ if (uris == null) {
3336
+ uris = shaka.hls.Utils.constructSegmentUris(getUris(),
3337
+ hlsSegment.verbatimSegmentUri, variables);
3175
3338
  }
3176
- return absoluteSegmentUri.length ? [absoluteSegmentUri] : [];
3339
+ return uris || [];
3177
3340
  };
3178
3341
 
3342
+ const allPartialSegments = partialSegmentRefs.length > 0 &&
3343
+ !!hlsSegment.verbatimSegmentUri;
3344
+
3179
3345
  const reference = new shaka.media.SegmentReference(
3180
3346
  startTime,
3181
3347
  endTime,
3182
- getUris,
3348
+ getSegmentUris,
3183
3349
  startByte,
3184
3350
  endByte,
3185
3351
  initSegmentReference,
@@ -3192,6 +3358,7 @@ shaka.hls.HlsParser = class {
3192
3358
  syncTime,
3193
3359
  status,
3194
3360
  aes128Key,
3361
+ allPartialSegments,
3195
3362
  );
3196
3363
 
3197
3364
  if (segmentWithByteRangeOptimization) {
@@ -3239,18 +3406,17 @@ shaka.hls.HlsParser = class {
3239
3406
  * get the bandwidth necessary for this segments If it's defined in the
3240
3407
  * playlist.
3241
3408
  *
3242
- * @param {string} verbatimMediaPlaylistUri
3243
3409
  * @param {!shaka.hls.Playlist} playlist
3244
- * @param {string} type
3245
- * @param {string} mimeType
3410
+ * @param {shaka.extern.Stream} stream
3246
3411
  * @param {!Map.<number, number>} mediaSequenceToStartTime
3247
3412
  * @param {!Map.<string, string>} variables
3413
+ * @param {function():!Array.<string>} getUris
3248
3414
  * @return {{segments: !Array.<!shaka.media.SegmentReference>,
3249
3415
  * bandwidth: (number|undefined)}}
3250
3416
  * @private
3251
3417
  */
3252
- createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
3253
- mediaSequenceToStartTime, variables) {
3418
+ createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
3419
+ getUris) {
3254
3420
  /** @type {Array.<!shaka.hls.Segment>} */
3255
3421
  const hlsSegments = playlist.segments;
3256
3422
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -3302,7 +3468,8 @@ shaka.hls.HlsParser = class {
3302
3468
  for (const drmTag of item.tags) {
3303
3469
  if (drmTag.name == 'EXT-X-KEY') {
3304
3470
  if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
3305
- aes128Key = this.parseAES128DrmTag_(drmTag, playlist);
3471
+ aes128Key =
3472
+ this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
3306
3473
  } else {
3307
3474
  aes128Key = undefined;
3308
3475
  }
@@ -3312,7 +3479,7 @@ shaka.hls.HlsParser = class {
3312
3479
  mediaSequenceToStartTime.set(position, startTime);
3313
3480
 
3314
3481
  initSegmentRef = this.getInitSegmentReference_(playlist,
3315
- item.tags, variables);
3482
+ item.tags, getUris, variables);
3316
3483
 
3317
3484
  // If the stream is low latency and the user has not configured the
3318
3485
  // lowLatencyMode, but if it has been configured to activate the
@@ -3332,9 +3499,9 @@ shaka.hls.HlsParser = class {
3332
3499
  item,
3333
3500
  startTime,
3334
3501
  variables,
3335
- playlist.absoluteUri,
3336
- type,
3337
- mimeType,
3502
+ playlist,
3503
+ stream,
3504
+ getUris,
3338
3505
  aes128Key);
3339
3506
 
3340
3507
  if (reference) {
@@ -3468,42 +3635,6 @@ shaka.hls.HlsParser = class {
3468
3635
  };
3469
3636
  }
3470
3637
 
3471
- /**
3472
- * Replaces the variables of a given URI.
3473
- *
3474
- * @param {string} uri
3475
- * @param {!Map.<string, string>} variables
3476
- * @return {string}
3477
- * @private
3478
- */
3479
- variableSubstitution_(uri, variables) {
3480
- if (!variables.size) {
3481
- return uri;
3482
- }
3483
- let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
3484
-
3485
- const uriVariables = newUri.match(/{\$\w*}/g);
3486
- if (uriVariables) {
3487
- for (const variable of uriVariables) {
3488
- // Note: All variables have the structure {$...}
3489
- const variableName = variable.slice(2, variable.length - 1);
3490
- const replaceValue = variables.get(variableName);
3491
- if (replaceValue) {
3492
- newUri = newUri.replace(variable, replaceValue);
3493
- } else {
3494
- shaka.log.error('A variable has been found that is not declared',
3495
- variableName);
3496
- throw new shaka.util.Error(
3497
- shaka.util.Error.Severity.CRITICAL,
3498
- shaka.util.Error.Category.MANIFEST,
3499
- shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND,
3500
- variableName);
3501
- }
3502
- }
3503
- }
3504
- return newUri;
3505
- }
3506
-
3507
3638
  /**
3508
3639
  * Attempts to guess stream's mime type based on content type and URI.
3509
3640
  *
@@ -3537,6 +3668,10 @@ shaka.hls.HlsParser = class {
3537
3668
  }
3538
3669
  }
3539
3670
 
3671
+ if (codecs == 'mjpg') {
3672
+ return 'application/mp4';
3673
+ }
3674
+
3540
3675
  // Not enough information to guess from the content type and codecs.
3541
3676
  return null;
3542
3677
  }
@@ -3569,10 +3704,11 @@ shaka.hls.HlsParser = class {
3569
3704
  * @param {string} codecs
3570
3705
  * @param {!shaka.hls.Playlist} playlist
3571
3706
  * @param {!Map.<string, string>} variables
3707
+ * @param {function():!Array.<string>} getUris
3572
3708
  * @return {!Promise.<string>}
3573
3709
  * @private
3574
3710
  */
3575
- async guessMimeType_(contentType, codecs, playlist, variables) {
3711
+ async guessMimeType_(contentType, codecs, playlist, variables, getUris) {
3576
3712
  const HlsParser = shaka.hls.HlsParser;
3577
3713
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
3578
3714
 
@@ -3582,10 +3718,13 @@ shaka.hls.HlsParser = class {
3582
3718
  goog.asserts.assert(playlist.segments.length,
3583
3719
  'Playlist should have segments!');
3584
3720
  const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
3585
- const middleSegmentUri = this.variableSubstitution_(
3586
- playlist.segments[middleSegmentIdx].absoluteUri, variables);
3587
3721
 
3588
- const parsedUri = new goog.Uri(middleSegmentUri);
3722
+ const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
3723
+ getUris(),
3724
+ playlist.segments[middleSegmentIdx].verbatimSegmentUri,
3725
+ variables);
3726
+
3727
+ const parsedUri = new goog.Uri(middleSegmentUris[0]);
3589
3728
  const extension = parsedUri.getPath().split('.').pop();
3590
3729
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
3591
3730
 
@@ -3608,7 +3747,7 @@ shaka.hls.HlsParser = class {
3608
3747
  // If unable to guess mime type, request a segment and try getting it
3609
3748
  // from the response.
3610
3749
  const headRequest = shaka.net.NetworkingEngine.makeRequest(
3611
- [middleSegmentUri], this.config_.retryParameters);
3750
+ middleSegmentUris, this.config_.retryParameters);
3612
3751
  headRequest.method = 'HEAD';
3613
3752
  const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
3614
3753
  const response = await this.makeNetworkRequest_(
@@ -3650,14 +3789,17 @@ shaka.hls.HlsParser = class {
3650
3789
  * @param {?string} height
3651
3790
  * @param {?string} frameRate
3652
3791
  * @param {?string} videoRange
3792
+ * @param {?string} videoLayout
3653
3793
  * @private
3654
3794
  */
3655
- addVideoAttributes_(stream, width, height, frameRate, videoRange) {
3795
+ addVideoAttributes_(stream, width, height, frameRate, videoRange,
3796
+ videoLayout) {
3656
3797
  if (stream) {
3657
3798
  stream.width = Number(width) || undefined;
3658
3799
  stream.height = Number(height) || undefined;
3659
3800
  stream.frameRate = Number(frameRate) || undefined;
3660
3801
  stream.hdr = videoRange || undefined;
3802
+ stream.videoLayout = videoLayout || undefined;
3661
3803
  }
3662
3804
  }
3663
3805
 
@@ -3665,16 +3807,16 @@ shaka.hls.HlsParser = class {
3665
3807
  * Makes a network request for the manifest and returns a Promise
3666
3808
  * with the resulting data.
3667
3809
  *
3668
- * @param {string} absoluteUri
3810
+ * @param {!Array.<string>} uris
3669
3811
  * @param {boolean=} isPlaylist
3670
3812
  * @return {!Promise.<!shaka.extern.Response>}
3671
3813
  * @private
3672
3814
  */
3673
- requestManifest_(absoluteUri, isPlaylist) {
3815
+ requestManifest_(uris, isPlaylist) {
3674
3816
  const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
3675
3817
 
3676
3818
  const request = shaka.net.NetworkingEngine.makeRequest(
3677
- [absoluteUri], this.config_.retryParameters);
3819
+ uris, this.config_.retryParameters);
3678
3820
 
3679
3821
  const type = isPlaylist ?
3680
3822
  shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
@@ -3960,8 +4102,8 @@ shaka.hls.HlsParser = class {
3960
4102
  * @typedef {{
3961
4103
  * stream: !shaka.extern.Stream,
3962
4104
  * type: string,
3963
- * verbatimMediaPlaylistUri: string,
3964
- * absoluteMediaPlaylistUri: string,
4105
+ * redirectUris: !Array.<string>,
4106
+ * getUris: function():!Array.<string>,
3965
4107
  * minTimestamp: number,
3966
4108
  * maxTimestamp: number,
3967
4109
  * mediaSequenceToStartTime: !Map.<number, number>,
@@ -3981,14 +4123,10 @@ shaka.hls.HlsParser = class {
3981
4123
  * The Stream itself.
3982
4124
  * @property {string} type
3983
4125
  * The type value. Could be 'video', 'audio', 'text', or 'image'.
3984
- * @property {string} verbatimMediaPlaylistUri
3985
- * The verbatim media playlist URI, as it appeared in the master playlist.
3986
- * This has not been canonicalized into an absolute URI. This gives us a
3987
- * consistent key for this playlist, even if redirects cause us to update
3988
- * from different origins each time.
3989
- * @property {string} absoluteMediaPlaylistUri
3990
- * The absolute media playlist URI, resolved relative to the master playlist
3991
- * and updated to reflect any redirects.
4126
+ * @property {!Array.<string>} redirectUris
4127
+ * The redirect URIs.
4128
+ * @property {function():!Array.<string>} getUris
4129
+ * The verbatim media playlist URIs, as it appeared in the master playlist.
3992
4130
  * @property {number} minTimestamp
3993
4131
  * The minimum timestamp found in the stream.
3994
4132
  * @property {number} maxTimestamp
@@ -4030,30 +4168,6 @@ shaka.hls.HlsParser.StreamInfo;
4030
4168
  shaka.hls.HlsParser.StreamInfos;
4031
4169
 
4032
4170
 
4033
- /**
4034
- * @typedef {{
4035
- * type: string,
4036
- * mimeType: string,
4037
- * codecs: string,
4038
- * language: ?string,
4039
- * height: ?string,
4040
- * width: ?string,
4041
- * channelCount: ?number,
4042
- * sampleRate: ?number
4043
- * }}
4044
- *
4045
- * @property {string} type
4046
- * @property {string} mimeType
4047
- * @property {string} codecs
4048
- * @property {?string} language
4049
- * @property {?string} height
4050
- * @property {?string} width
4051
- * @property {?number} channelCount
4052
- * @property {?number} sampleRate
4053
- */
4054
- shaka.hls.HlsParser.BasicInfo;
4055
-
4056
-
4057
4171
  /**
4058
4172
  * @const {!Object.<string, string>}
4059
4173
  * @private
@@ -4175,8 +4289,6 @@ shaka.hls.HlsParser.PresentationType_ = {
4175
4289
  LIVE: 'LIVE',
4176
4290
  };
4177
4291
 
4178
- shaka.media.ManifestParser.registerParserByExtension(
4179
- 'm3u8', () => new shaka.hls.HlsParser());
4180
4292
  shaka.media.ManifestParser.registerParserByMime(
4181
4293
  'application/x-mpegurl', () => new shaka.hls.HlsParser());
4182
4294
  shaka.media.ManifestParser.registerParserByMime(