shaka-player 4.5.0 → 4.6.1

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 +109 -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 +34 -30
  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 +789 -738
  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 +1147 -1113
  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 +857 -791
  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 +1359 -1314
  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 +251 -84
  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 +92 -29
  70. package/lib/hls/hls_classes.js +28 -43
  71. package/lib/hls/hls_parser.js +770 -652
  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 +72 -44
  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 +998 -1503
  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 +417 -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');
1406
1350
 
1407
- const streamInfos = this.createStreamInfosForVariantTag_(tag,
1408
- resolution, frameRate);
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';
1365
+
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*/);
@@ -1643,9 +1672,15 @@ shaka.hls.HlsParser = class {
1643
1672
  // the value NONE. If the value is a quoted-string, it MUST match the
1644
1673
  // value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the
1645
1674
  // Playlist whose TYPE attribute is CLOSED-CAPTIONS.
1646
- if (type == ContentType.VIDEO && closedCaptionsAttr &&
1647
- closedCaptionsAttr != 'NONE') {
1648
- return this.groupIdToClosedCaptionsMap_.get(closedCaptionsAttr);
1675
+ if (type == ContentType.VIDEO) {
1676
+ if (closedCaptionsAttr && closedCaptionsAttr != 'NONE') {
1677
+ return this.groupIdToClosedCaptionsMap_.get(closedCaptionsAttr);
1678
+ } else if (!closedCaptionsAttr &&
1679
+ this.groupIdToClosedCaptionsMap_.size) {
1680
+ for (const key of this.groupIdToClosedCaptionsMap_.keys()) {
1681
+ return this.groupIdToClosedCaptionsMap_.get(key);
1682
+ }
1683
+ }
1649
1684
  }
1650
1685
  return null;
1651
1686
  }
@@ -1686,6 +1721,7 @@ shaka.hls.HlsParser = class {
1686
1721
  * @param {?string} height
1687
1722
  * @param {?string} frameRate
1688
1723
  * @param {?string} videoRange
1724
+ * @param {?string} videoLayout
1689
1725
  * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1690
1726
  * @param {!Set.<string>} keyIds
1691
1727
  * @return {!Array.<!shaka.extern.Variant>}
@@ -1693,13 +1729,13 @@ shaka.hls.HlsParser = class {
1693
1729
  */
1694
1730
  createVariants_(
1695
1731
  audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1696
- drmInfos, keyIds) {
1732
+ videoLayout, drmInfos, keyIds) {
1697
1733
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1698
1734
  const DrmEngine = shaka.media.DrmEngine;
1699
1735
 
1700
1736
  for (const info of videoInfos) {
1701
1737
  this.addVideoAttributes_(
1702
- info.stream, width, height, frameRate, videoRange);
1738
+ info.stream, width, height, frameRate, videoRange, videoLayout);
1703
1739
  }
1704
1740
 
1705
1741
  // In case of audio-only or video-only content or the audio/video is
@@ -1731,9 +1767,9 @@ shaka.hls.HlsParser = class {
1731
1767
  const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
1732
1768
  const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
1733
1769
  const videoStreamUri =
1734
- videoInfo ? videoInfo.verbatimMediaPlaylistUri : '';
1770
+ videoInfo ? videoInfo.getUris().sort().join(',') : '';
1735
1771
  const audioStreamUri =
1736
- audioInfo ? audioInfo.verbatimMediaPlaylistUri : '';
1772
+ audioInfo ? audioInfo.getUris().sort().join(',') : '';
1737
1773
  const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
1738
1774
 
1739
1775
  if (audioStream && videoStream) {
@@ -1821,34 +1857,59 @@ shaka.hls.HlsParser = class {
1821
1857
  /**
1822
1858
  * Parse EXT-X-MEDIA media tag into a Stream object.
1823
1859
  *
1824
- * @param {shaka.hls.Tag} tag
1860
+ * @param {!Array.<!shaka.hls.Tag>} tags
1861
+ * @param {!Map.<string, string>} groupIdPathwayIdMapping
1825
1862
  * @return {!shaka.hls.HlsParser.StreamInfo}
1826
1863
  * @private
1827
1864
  */
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');
1865
+ createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping) {
1866
+ const verbatimMediaPlaylistUris = [];
1867
+ const globalGroupIds = [];
1868
+ const groupIdUriMappping = new Map();
1869
+ for (const tag of tags) {
1870
+ goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1871
+ 'Should only be called on media tags!');
1872
+ const uri = tag.getRequiredAttrValue('URI');
1873
+ const groupId = tag.getRequiredAttrValue('GROUP-ID');
1874
+ verbatimMediaPlaylistUris.push(uri);
1875
+ globalGroupIds.push(groupId);
1876
+ groupIdUriMappping.set(groupId, uri);
1877
+ }
1878
+
1879
+ const globalGroupId = globalGroupIds.sort().join(',');
1880
+ const firstTag = tags[0];
1832
1881
  let codecs = '';
1833
1882
  /** @type {string} */
1834
- const type = this.getType_(tag);
1883
+ const type = this.getType_(firstTag);
1835
1884
  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);
1885
+ codecs = firstTag.getAttributeValue('CODECS') || '';
1886
+ } else {
1887
+ for (const groupId of globalGroupIds) {
1888
+ if (this.groupIdToCodecsMap_.has(groupId)) {
1889
+ codecs = this.groupIdToCodecsMap_.get(groupId);
1890
+ break;
1891
+ }
1892
+ }
1839
1893
  }
1840
1894
 
1841
- const verbatimMediaPlaylistUri = this.variableSubstitution_(
1842
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
1843
-
1844
1895
  // Check if the stream has already been created as part of another Variant
1845
1896
  // and return it if it has.
1846
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1847
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1897
+ const key = verbatimMediaPlaylistUris.sort().join(',');
1898
+ if (this.uriToStreamInfosMap_.has(key)) {
1899
+ return this.uriToStreamInfosMap_.get(key);
1900
+ }
1901
+ const streamId = this.globalId_++;
1902
+ if (this.contentSteeringManager_) {
1903
+ for (const [groupId, uri] of groupIdUriMappping) {
1904
+ const pathwayId = groupIdPathwayIdMapping.get(groupId);
1905
+ if (pathwayId) {
1906
+ this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
1907
+ }
1908
+ }
1848
1909
  }
1849
1910
 
1850
- const language = tag.getAttributeValue('LANGUAGE');
1851
- const name = tag.getAttributeValue('NAME');
1911
+ const language = firstTag.getAttributeValue('LANGUAGE');
1912
+ const name = firstTag.getAttributeValue('NAME');
1852
1913
 
1853
1914
  // NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
1854
1915
  // However, we don't bother to validate "AUTOSELECT", since we don't
@@ -1856,39 +1917,39 @@ shaka.hls.HlsParser = class {
1856
1917
  // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
1857
1918
  // only be selected explicitly by the user, and we don't have a way to
1858
1919
  // represent that in our model.
1859
- const defaultAttrValue = tag.getAttributeValue('DEFAULT');
1920
+ const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
1860
1921
  const primary = defaultAttrValue == 'YES';
1861
1922
 
1862
- const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
1863
- const spatialAudio = type == 'audio' ? this.isSpatialAudio_(tag) : false;
1864
- const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1923
+ const channelsCount =
1924
+ type == 'audio' ? this.getChannelsCount_(firstTag) : null;
1925
+ const spatialAudio =
1926
+ type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
1927
+ const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
1865
1928
 
1866
- const forcedAttrValue = tag.getAttributeValue('FORCED');
1929
+ const forcedAttrValue = firstTag.getAttributeValue('FORCED');
1867
1930
  const forced = forcedAttrValue == 'YES';
1868
- const sampleRate = type == 'audio' ? this.getSampleRate_(tag) : null;
1931
+ const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
1869
1932
  // TODO: Should we take into account some of the currently ignored
1870
1933
  // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
1871
1934
  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);
1935
+ streamId, verbatimMediaPlaylistUris, codecs, type, language,
1936
+ primary, name, channelsCount, /* closedCaptions= */ null,
1937
+ characteristics, forced, sampleRate, spatialAudio);
1938
+ if (streamInfo.stream) {
1939
+ streamInfo.stream.groupId = globalGroupId;
1940
+ }
1941
+ if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
1942
+ this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
1877
1943
  } else {
1878
- this.groupIdToStreamInfosMap_.set(groupId, [streamInfo]);
1944
+ this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
1879
1945
  }
1880
1946
 
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);
1947
+ this.uriToStreamInfosMap_.set(key, streamInfo);
1887
1948
  return streamInfo;
1888
1949
  }
1889
1950
 
1890
1951
  /**
1891
- * Parse EXT-X-MEDIA media tag into a Stream object.
1952
+ * Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
1892
1953
  *
1893
1954
  * @param {shaka.hls.Tag} tag
1894
1955
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
@@ -1900,8 +1961,7 @@ shaka.hls.HlsParser = class {
1900
1961
  /** @type {string} */
1901
1962
  const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
1902
1963
 
1903
- const verbatimImagePlaylistUri = this.variableSubstitution_(
1904
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
1964
+ const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
1905
1965
  const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
1906
1966
 
1907
1967
  // Check if the stream has already been created as part of another Variant
@@ -1916,16 +1976,10 @@ shaka.hls.HlsParser = class {
1916
1976
  const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1917
1977
 
1918
1978
  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
- }
1979
+ this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
1980
+ /* primary= */ false, name, /* channelsCount= */ null,
1981
+ /* closedCaptions= */ null, characteristics, /* forced= */ false,
1982
+ /* sampleRate= */ null, /* spatialAudio= */ false);
1929
1983
 
1930
1984
  // Parse misc attributes.
1931
1985
  const resolution = tag.getAttributeValue('RESOLUTION');
@@ -1958,45 +2012,104 @@ shaka.hls.HlsParser = class {
1958
2012
  return streamInfo;
1959
2013
  }
1960
2014
 
2015
+ /**
2016
+ * Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
2017
+ *
2018
+ * @param {shaka.hls.Tag} tag
2019
+ * @return {!shaka.hls.HlsParser.StreamInfo}
2020
+ * @private
2021
+ */
2022
+ createStreamInfoFromIframeTag_(tag) {
2023
+ goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
2024
+ 'Should only be called on iframe tags!');
2025
+ /** @type {string} */
2026
+ const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
2027
+
2028
+ const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
2029
+ const codecs = tag.getAttributeValue('CODECS') || '';
2030
+
2031
+ // Check if the stream has already been created as part of another Variant
2032
+ // and return it if it has.
2033
+ if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
2034
+ return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
2035
+ }
2036
+
2037
+ const language = tag.getAttributeValue('LANGUAGE');
2038
+ const name = tag.getAttributeValue('NAME');
2039
+
2040
+ const characteristics = tag.getAttributeValue('CHARACTERISTICS');
2041
+
2042
+ const streamInfo = this.createStreamInfo_(
2043
+ this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
2044
+ /* primary= */ false, name, /* channelsCount= */ null,
2045
+ /* closedCaptions= */ null, characteristics, /* forced= */ false,
2046
+ /* sampleRate= */ null, /* spatialAudio= */ false);
2047
+
2048
+ // Parse misc attributes.
2049
+ const resolution = tag.getAttributeValue('RESOLUTION');
2050
+ const [width, height] = resolution ? resolution.split('x') : [null, null];
2051
+ streamInfo.stream.width = Number(width) || undefined;
2052
+ streamInfo.stream.height = Number(height) || undefined;
2053
+ const bandwidth = tag.getAttributeValue('BANDWIDTH');
2054
+ if (bandwidth) {
2055
+ streamInfo.stream.bandwidth = Number(bandwidth);
2056
+ }
2057
+
2058
+ this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
2059
+ return streamInfo;
2060
+ }
2061
+
1961
2062
  /**
1962
2063
  * Parse an EXT-X-STREAM-INF media tag into a Stream object.
1963
2064
  *
1964
- * @param {!shaka.hls.Tag} tag
2065
+ * @param {!Array.<!shaka.hls.Tag>} tags
1965
2066
  * @param {!Array.<string>} allCodecs
1966
2067
  * @param {string} type
2068
+ * @param {?string} language
2069
+ * @param {?string} name
2070
+ * @param {?number} channelsCount
2071
+ * @param {?string} characteristics
2072
+ * @param {?number} sampleRate
2073
+ * @param {boolean} spatialAudio
1967
2074
  * @return {!shaka.hls.HlsParser.StreamInfo}
1968
2075
  * @private
1969
2076
  */
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_);
2077
+ createStreamInfoFromVariantTags_(tags, allCodecs, type, language, name,
2078
+ channelsCount, characteristics, sampleRate, spatialAudio) {
2079
+ const streamId = this.globalId_++;
2080
+ const verbatimMediaPlaylistUris = [];
2081
+ for (const tag of tags) {
2082
+ goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
2083
+ 'Should only be called on variant tags!');
2084
+ const uri = tag.getRequiredAttrValue('URI');
2085
+ const pathwayId = tag.getAttributeValue('PATHWAY-ID');
2086
+ if (this.contentSteeringManager_ && pathwayId) {
2087
+ this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
2088
+ }
2089
+ verbatimMediaPlaylistUris.push(uri);
2090
+ }
1975
2091
 
1976
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1977
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
2092
+ const key = verbatimMediaPlaylistUris.sort().join(',');
2093
+ if (this.uriToStreamInfosMap_.has(key)) {
2094
+ return this.uriToStreamInfosMap_.get(key);
1978
2095
  }
1979
2096
 
1980
- const closedCaptions = this.getClosedCaptions_(tag, type);
2097
+ const closedCaptions = this.getClosedCaptions_(tags[0], type);
1981
2098
  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
- }
2099
+ const streamInfo = this.createStreamInfo_(
2100
+ streamId, verbatimMediaPlaylistUris, codecs, type, language,
2101
+ /* primary= */ false, name, channelsCount, closedCaptions,
2102
+ characteristics, /* forced= */ false, sampleRate,
2103
+ /* spatialAudio= */ false);
1992
2104
 
1993
- this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
2105
+ this.uriToStreamInfosMap_.set(key, streamInfo);
1994
2106
  return streamInfo;
1995
2107
  }
1996
2108
 
1997
2109
 
1998
2110
  /**
1999
- * @param {string} verbatimMediaPlaylistUri
2111
+ * @param {number} streamId
2112
+ * @param {!Array.<string>} verbatimMediaPlaylistUris
2000
2113
  * @param {string} codecs
2001
2114
  * @param {string} type
2002
2115
  * @param {?string} languageValue
@@ -2011,25 +2124,34 @@ shaka.hls.HlsParser = class {
2011
2124
  * @return {!shaka.hls.HlsParser.StreamInfo}
2012
2125
  * @private
2013
2126
  */
2014
- createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, languageValue,
2015
- primary, name, channelsCount, closedCaptions, characteristics, forced,
2016
- sampleRate, spatialAudio) {
2127
+ createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
2128
+ languageValue, primary, name, channelsCount, closedCaptions,
2129
+ characteristics, forced, sampleRate, spatialAudio) {
2017
2130
  // TODO: Refactor, too many parameters
2018
- const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
2019
- this.masterPlaylistUri_, verbatimMediaPlaylistUri);
2020
2131
 
2021
2132
  // This stream is lazy-loaded inside the createSegmentIndex function.
2022
2133
  // So we start out with a stream object that does not contain the actual
2023
2134
  // 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);
2135
+ const stream = this.makeStreamObject_(streamId, codecs, type,
2136
+ languageValue, primary, name, channelsCount, closedCaptions,
2137
+ characteristics, forced, sampleRate, spatialAudio);
2138
+
2139
+ const redirectUris = [];
2140
+ const getUris = () => {
2141
+ if (this.contentSteeringManager_ &&
2142
+ verbatimMediaPlaylistUris.length > 1) {
2143
+ return this.contentSteeringManager_.getLocations(streamId);
2144
+ }
2145
+ return redirectUris.concat(shaka.hls.Utils.constructUris(
2146
+ [this.masterPlaylistUri_], verbatimMediaPlaylistUris,
2147
+ this.globalVariables_));
2148
+ };
2027
2149
  const streamInfo = {
2028
2150
  stream,
2029
2151
  type,
2030
- verbatimMediaPlaylistUri,
2152
+ redirectUris,
2153
+ getUris,
2031
2154
  // These values are filled out or updated after lazy-loading:
2032
- absoluteMediaPlaylistUri: initialMediaPlaylistUri,
2033
2155
  minTimestamp: 0,
2034
2156
  maxTimestamp: 0,
2035
2157
  mediaSequenceToStartTime: new Map(),
@@ -2044,27 +2166,45 @@ shaka.hls.HlsParser = class {
2044
2166
 
2045
2167
  /** @param {!AbortSignal} abortSignal */
2046
2168
  const downloadSegmentIndex = async (abortSignal) => {
2169
+ const uris = streamInfo.getUris();
2047
2170
  // Download the actual manifest.
2048
2171
  const response = await this.requestManifest_(
2049
- streamInfo.absoluteMediaPlaylistUri, /* isPlaylist= */ true);
2172
+ streamInfo.getUris(), /* isPlaylist= */ true);
2050
2173
  if (abortSignal.aborted) {
2051
2174
  return;
2052
2175
  }
2053
2176
 
2054
2177
  // Record the final URI after redirects.
2055
- const absoluteMediaPlaylistUri = response.uri;
2178
+ const responseUri = response.uri;
2179
+ if (responseUri != response.originalUri && !uris.includes(responseUri)) {
2180
+ redirectUris.push(responseUri);
2181
+ }
2056
2182
 
2057
2183
  // Record the redirected, final URI of this media playlist when we parse
2058
2184
  // it.
2059
2185
  /** @type {!shaka.hls.Playlist} */
2060
- const playlist = this.manifestTextParser_.parsePlaylist(
2061
- response.data, absoluteMediaPlaylistUri);
2186
+ const playlist = this.manifestTextParser_.parsePlaylist(response.data);
2187
+
2188
+ let mimeType = undefined;
2189
+
2190
+ // If no codec info was provided in the manifest and codec guessing is
2191
+ // disabled we try to get necessary info from the media data.
2192
+ if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
2193
+ const basicInfo =
2194
+ await this.getMediaPlaylistBasicInfo_(playlist, getUris);
2195
+
2196
+ goog.asserts.assert(
2197
+ type === basicInfo.type, 'Media types should match!');
2198
+
2199
+ mimeType = basicInfo.mimeType;
2200
+ codecs = basicInfo.codecs;
2201
+ }
2062
2202
 
2063
2203
  const wasLive = this.isLive_();
2064
2204
  const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
2065
- playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
2205
+ streamId, playlist, getUris, responseUri, codecs,
2066
2206
  type, languageValue, primary, name, channelsCount, closedCaptions,
2067
- characteristics, forced, sampleRate, spatialAudio);
2207
+ characteristics, forced, sampleRate, spatialAudio, mimeType);
2068
2208
  if (abortSignal.aborted) {
2069
2209
  return;
2070
2210
  }
@@ -2078,7 +2218,6 @@ shaka.hls.HlsParser = class {
2078
2218
  }
2079
2219
 
2080
2220
  // Copy values from the real stream info to our initial one.
2081
- streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
2082
2221
  streamInfo.minTimestamp = realStreamInfo.minTimestamp;
2083
2222
  streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
2084
2223
  streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
@@ -2095,6 +2234,7 @@ shaka.hls.HlsParser = class {
2095
2234
  stream.keyIds = realStream.keyIds;
2096
2235
  stream.mimeType = realStream.mimeType;
2097
2236
  stream.bandwidth = realStream.bandwidth;
2237
+ stream.codecs = realStream.codecs || stream.codecs;
2098
2238
 
2099
2239
  // Since we lazy-loaded this content, the player may need to create new
2100
2240
  // sessions for the DRM info in this stream.
@@ -2296,9 +2436,10 @@ shaka.hls.HlsParser = class {
2296
2436
  }
2297
2437
 
2298
2438
  /**
2439
+ * @param {number} streamId
2299
2440
  * @param {!shaka.hls.Playlist} playlist
2300
- * @param {string} verbatimMediaPlaylistUri
2301
- * @param {string} absoluteMediaPlaylistUri
2441
+ * @param {function():!Array.<string>} getUris
2442
+ * @param {string} responseUri
2302
2443
  * @param {string} codecs
2303
2444
  * @param {string} type
2304
2445
  * @param {?string} languageValue
@@ -2314,8 +2455,8 @@ shaka.hls.HlsParser = class {
2314
2455
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
2315
2456
  * @private
2316
2457
  */
2317
- async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri,
2318
- absoluteMediaPlaylistUri, codecs, type, languageValue, primary, name,
2458
+ async convertParsedPlaylistIntoStreamInfo_(streamId, playlist,
2459
+ getUris, responseUri, codecs, type, languageValue, primary, name,
2319
2460
  channelsCount, closedCaptions, characteristics, forced, sampleRate,
2320
2461
  spatialAudio, mimeType = undefined) {
2321
2462
  if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
@@ -2331,7 +2472,8 @@ shaka.hls.HlsParser = class {
2331
2472
  const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
2332
2473
  'EXT-X-DEFINE');
2333
2474
 
2334
- const mediaVariables = this.parseMediaVariables_(variablesTags);
2475
+ const mediaVariables =
2476
+ this.parseMediaVariables_(variablesTags, responseUri);
2335
2477
 
2336
2478
  goog.asserts.assert(playlist.segments != null,
2337
2479
  'Media playlist should have segments!');
@@ -2344,7 +2486,7 @@ shaka.hls.HlsParser = class {
2344
2486
 
2345
2487
  if (!mimeType) {
2346
2488
  mimeType = await this.guessMimeType_(type, codecs, playlist,
2347
- mediaVariables);
2489
+ mediaVariables, getUris);
2348
2490
  }
2349
2491
 
2350
2492
  const {drmInfos, keyIds, encrypted, aesEncrypted} =
@@ -2357,11 +2499,20 @@ shaka.hls.HlsParser = class {
2357
2499
  shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
2358
2500
  }
2359
2501
 
2502
+ const stream = this.makeStreamObject_(streamId, codecs, type,
2503
+ languageValue, primary, name, channelsCount, closedCaptions,
2504
+ characteristics, forced, sampleRate, spatialAudio);
2505
+ stream.encrypted = encrypted;
2506
+ stream.drmInfos = drmInfos;
2507
+ stream.keyIds = keyIds;
2508
+ stream.mimeType = mimeType;
2509
+
2360
2510
  const mediaSequenceToStartTime = this.isLive_() ?
2361
2511
  this.mediaSequenceToStartTimeByType_.get(type) : new Map();
2512
+
2362
2513
  const {segments, bandwidth} = this.createSegments_(
2363
- verbatimMediaPlaylistUri, playlist, type, mimeType,
2364
- mediaSequenceToStartTime, mediaVariables);
2514
+ playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
2515
+ stream.bandwidth = bandwidth;
2365
2516
 
2366
2517
  // This new calculation is necessary for Low Latency streams.
2367
2518
  if (this.isLive_()) {
@@ -2373,6 +2524,7 @@ shaka.hls.HlsParser = class {
2373
2524
  const lastEndTime = lastSegment.endTime;
2374
2525
  /** @type {!shaka.media.SegmentIndex} */
2375
2526
  const segmentIndex = new shaka.media.SegmentIndex(segments);
2527
+ stream.segmentIndex = segmentIndex;
2376
2528
 
2377
2529
  const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
2378
2530
  playlist.tags, 'EXT-X-SERVER-CONTROL');
@@ -2387,21 +2539,11 @@ shaka.hls.HlsParser = class {
2387
2539
  const {nextMediaSequence, nextPart} =
2388
2540
  this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
2389
2541
 
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
2542
  return {
2401
2543
  stream,
2402
2544
  type,
2403
- verbatimMediaPlaylistUri,
2404
- absoluteMediaPlaylistUri,
2545
+ redirectUris: [],
2546
+ getUris,
2405
2547
  minTimestamp: firstStartTime,
2406
2548
  maxTimestamp: lastEndTime,
2407
2549
  canSkipSegments,
@@ -2465,6 +2607,7 @@ shaka.hls.HlsParser = class {
2465
2607
  * The parameters that are passed into here are only the things that can be
2466
2608
  * known without downloading the media playlist; other values must be set
2467
2609
  * manually on the object after creation.
2610
+ * @param {number} id
2468
2611
  * @param {string} codecs
2469
2612
  * @param {string} type
2470
2613
  * @param {?string} languageValue
@@ -2479,8 +2622,9 @@ shaka.hls.HlsParser = class {
2479
2622
  * @return {!shaka.extern.Stream}
2480
2623
  * @private
2481
2624
  */
2482
- makeStreamObject_(codecs, type, languageValue, primary, name, channelsCount,
2483
- closedCaptions, characteristics, forced, sampleRate, spatialAudio) {
2625
+ makeStreamObject_(id, codecs, type, languageValue, primary, name,
2626
+ channelsCount, closedCaptions, characteristics, forced, sampleRate,
2627
+ spatialAudio) {
2484
2628
  // Fill out a "best-guess" mimeType, for now. It will be replaced once the
2485
2629
  // stream is lazy-loaded.
2486
2630
  const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
@@ -2492,8 +2636,21 @@ shaka.hls.HlsParser = class {
2492
2636
  }
2493
2637
  }
2494
2638
 
2495
- const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2496
- shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
2639
+ let kind = undefined;
2640
+ let accessibilityPurpose = null;
2641
+ if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2642
+ if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
2643
+ roles.includes('public.accessibility.describes-music-and-sound')) {
2644
+ kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
2645
+ } else {
2646
+ kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
2647
+ }
2648
+ } else {
2649
+ if (roles.includes('public.accessibility.describes-video')) {
2650
+ accessibilityPurpose =
2651
+ shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
2652
+ }
2653
+ }
2497
2654
 
2498
2655
  // If there are no roles, and we have defaulted to the subtitle "kind" for
2499
2656
  // this track, add the implied subtitle role.
@@ -2535,9 +2692,11 @@ shaka.hls.HlsParser = class {
2535
2692
  spatialAudio,
2536
2693
  closedCaptions,
2537
2694
  hdr: undefined,
2695
+ videoLayout: undefined,
2538
2696
  tilesLayout: undefined,
2539
- accessibilityPurpose: null,
2697
+ accessibilityPurpose: accessibilityPurpose,
2540
2698
  external: false,
2699
+ fastSwitching: false,
2541
2700
  };
2542
2701
  }
2543
2702
 
@@ -2608,10 +2767,12 @@ shaka.hls.HlsParser = class {
2608
2767
  /**
2609
2768
  * @param {!shaka.hls.Tag} drmTag
2610
2769
  * @param {!shaka.hls.Playlist} playlist
2770
+ * @param {function():!Array.<string>} getUris
2771
+ * @param {?Map.<string, string>=} variables
2611
2772
  * @return {!shaka.extern.aes128Key}
2612
2773
  * @private
2613
2774
  */
2614
- parseAES128DrmTag_(drmTag, playlist) {
2775
+ parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
2615
2776
  // Check if the Web Crypto API is available.
2616
2777
  if (!window.crypto || !window.crypto.subtle) {
2617
2778
  shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
@@ -2646,18 +2807,17 @@ shaka.hls.HlsParser = class {
2646
2807
  }
2647
2808
  }
2648
2809
 
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
2810
  const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
2657
2811
 
2658
2812
  // Don't download the key object until the segment is parsed, to avoid a
2659
2813
  // startup delay for long manifests with lots of keys.
2660
2814
  keyInfo.fetchKey = async () => {
2815
+ const keyUri = shaka.hls.Utils.constructSegmentUris(
2816
+ getUris(), drmTag.getRequiredAttrValue('URI'), variables);
2817
+
2818
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2819
+ const request = shaka.net.NetworkingEngine.makeRequest(
2820
+ [keyUri], this.config_.retryParameters);
2661
2821
  const keyResponse = await this.makeNetworkRequest_(request, requestType);
2662
2822
 
2663
2823
  // keyResponse.status is undefined when URI is "data:text/plain;base64,"
@@ -2834,11 +2994,12 @@ shaka.hls.HlsParser = class {
2834
2994
  * Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
2835
2995
  * @param {!shaka.hls.Playlist} playlist
2836
2996
  * @param {!Array.<!shaka.hls.Tag>} tags Segment tags
2837
- * @param {!Map.<string, string>} variables
2997
+ * @param {function():!Array.<string>} getUris
2998
+ * @param {?Map.<string, string>=} variables
2838
2999
  * @return {shaka.media.InitSegmentReference}
2839
3000
  * @private
2840
3001
  */
2841
- getInitSegmentReference_(playlist, tags, variables) {
3002
+ getInitSegmentReference_(playlist, tags, getUris, variables) {
2842
3003
  /** @type {?shaka.hls.Tag} */
2843
3004
  const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
2844
3005
 
@@ -2847,28 +3008,30 @@ shaka.hls.HlsParser = class {
2847
3008
  }
2848
3009
  // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
2849
3010
  const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
2850
- const absoluteInitSegmentUri = this.variableSubstitution_(
2851
- shaka.hls.Utils.constructAbsoluteUri(
2852
- playlist.absoluteUri, verbatimInitSegmentUri),
2853
- variables);
3011
+ const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
3012
+ getUris(), verbatimInitSegmentUri, variables);
2854
3013
 
2855
3014
  const mapTagKey = [
2856
- absoluteInitSegmentUri,
3015
+ absoluteInitSegmentUris.toString(),
2857
3016
  mapTag.getAttributeValue('BYTERANGE', ''),
2858
3017
  ].join('-');
2859
3018
  if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
2860
3019
  /** @type {shaka.extern.aes128Key|undefined} */
2861
3020
  let aes128Key = undefined;
3021
+ let byteRangeTag = null;
2862
3022
  for (const tag of tags) {
2863
3023
  if (tag.name == 'EXT-X-KEY') {
2864
3024
  if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
2865
3025
  tag.id < mapTag.id) {
2866
- aes128Key = this.parseAES128DrmTag_(tag, playlist);
3026
+ aes128Key =
3027
+ this.parseAES128DrmTag_(tag, playlist, getUris, variables);
2867
3028
  }
3029
+ } else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
3030
+ byteRangeTag = tag;
2868
3031
  }
2869
3032
  }
2870
3033
  const initSegmentRef = this.createInitSegmentReference_(
2871
- absoluteInitSegmentUri, mapTag, aes128Key);
3034
+ absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
2872
3035
  this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
2873
3036
  }
2874
3037
  return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
@@ -2877,16 +3040,21 @@ shaka.hls.HlsParser = class {
2877
3040
  /**
2878
3041
  * Create an InitSegmentReference object for the EXT-X-MAP tag in the media
2879
3042
  * playlist.
2880
- * @param {string} absoluteInitSegmentUri
3043
+ * @param {!Array.<string>} absoluteInitSegmentUris
2881
3044
  * @param {!shaka.hls.Tag} mapTag EXT-X-MAP
3045
+ * @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
2882
3046
  * @param {shaka.extern.aes128Key=} aes128Key
2883
3047
  * @return {!shaka.media.InitSegmentReference}
2884
3048
  * @private
2885
3049
  */
2886
- createInitSegmentReference_(absoluteInitSegmentUri, mapTag, aes128Key) {
3050
+ createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
3051
+ aes128Key) {
2887
3052
  let startByte = 0;
2888
3053
  let endByte = null;
2889
- const byterange = mapTag.getAttributeValue('BYTERANGE');
3054
+ let byterange = mapTag.getAttributeValue('BYTERANGE');
3055
+ if (!byterange && byteRangeTag) {
3056
+ byterange = byteRangeTag.value;
3057
+ }
2890
3058
  // If a BYTERANGE attribute is not specified, the segment consists
2891
3059
  // of the entire resource.
2892
3060
  if (byterange) {
@@ -2907,7 +3075,7 @@ shaka.hls.HlsParser = class {
2907
3075
  }
2908
3076
 
2909
3077
  const initSegmentRef = new shaka.media.InitSegmentReference(
2910
- () => [absoluteInitSegmentUri],
3078
+ () => absoluteInitSegmentUris,
2911
3079
  startByte,
2912
3080
  endByte,
2913
3081
  /* mediaQuality= */ null,
@@ -2925,16 +3093,16 @@ shaka.hls.HlsParser = class {
2925
3093
  * @param {!shaka.hls.Segment} hlsSegment
2926
3094
  * @param {number} startTime
2927
3095
  * @param {!Map.<string, string>} variables
2928
- * @param {string} absoluteMediaPlaylistUri
2929
- * @param {string} type
2930
- * @param {string} mimeType
3096
+ * @param {!shaka.hls.Playlist} playlist
3097
+ * @param {shaka.extern.Stream} stream
3098
+ * @param {function():!Array.<string>} getUris
2931
3099
  * @param {shaka.extern.aes128Key=} aes128Key
2932
3100
  * @return {shaka.media.SegmentReference}
2933
3101
  * @private
2934
3102
  */
2935
3103
  createSegmentReference_(
2936
3104
  initSegmentReference, previousReference, hlsSegment, startTime,
2937
- variables, absoluteMediaPlaylistUri, type, mimeType, aes128Key) {
3105
+ variables, playlist, stream, getUris, aes128Key) {
2938
3106
  const tags = hlsSegment.tags;
2939
3107
  const extinfTag =
2940
3108
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
@@ -2991,8 +3159,9 @@ shaka.hls.HlsParser = class {
2991
3159
  let isPreloadSegment = false;
2992
3160
 
2993
3161
  if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
2994
- const byterangeOptimizationSupport = (mimeType == 'video/mp4' ||
2995
- mimeType == 'audio/mp4') && window.ReadableStream;
3162
+ const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' ||
3163
+ stream.mimeType == 'audio/mp4') && window.ReadableStream &&
3164
+ this.config_.hls.allowLowLatencyByteRangeOptimization;
2996
3165
 
2997
3166
  let partialSyncTime = syncTime;
2998
3167
  for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
@@ -3048,14 +3217,14 @@ shaka.hls.HlsParser = class {
3048
3217
  somePartialSegmentWithGap = true;
3049
3218
  }
3050
3219
 
3051
- let pAbsoluteUri = null;
3220
+ let uris = null;
3052
3221
  const getPartialUris = () => {
3053
- if (pAbsoluteUri == null) {
3222
+ if (uris == null) {
3054
3223
  goog.asserts.assert(pUri, 'Partial uri should be defined!');
3055
- pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri(
3056
- absoluteMediaPlaylistUri, pUri);
3224
+ uris = shaka.hls.Utils.constructSegmentUris(
3225
+ getUris(), pUri, variables);
3057
3226
  }
3058
- return [pAbsoluteUri];
3227
+ return uris;
3059
3228
  };
3060
3229
 
3061
3230
  if (byterangeOptimizationSupport &&
@@ -3150,7 +3319,7 @@ shaka.hls.HlsParser = class {
3150
3319
 
3151
3320
  let tilesLayout = '';
3152
3321
  let tileDuration = null;
3153
- if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
3322
+ if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
3154
3323
  // By default in HLS the tilesLayout is 1x1
3155
3324
  tilesLayout = '1x1';
3156
3325
  const tilesTag =
@@ -3164,22 +3333,25 @@ shaka.hls.HlsParser = class {
3164
3333
  }
3165
3334
  }
3166
3335
 
3167
- let absoluteSegmentUri = null;
3168
- const getUris = () => {
3336
+ let uris = null;
3337
+ const getSegmentUris = () => {
3169
3338
  if (getUrisOptimization) {
3170
3339
  return getUrisOptimization();
3171
3340
  }
3172
- if (absoluteSegmentUri == null) {
3173
- absoluteSegmentUri = this.variableSubstitution_(
3174
- hlsSegment.absoluteUri, variables);
3341
+ if (uris == null) {
3342
+ uris = shaka.hls.Utils.constructSegmentUris(getUris(),
3343
+ hlsSegment.verbatimSegmentUri, variables);
3175
3344
  }
3176
- return absoluteSegmentUri.length ? [absoluteSegmentUri] : [];
3345
+ return uris || [];
3177
3346
  };
3178
3347
 
3348
+ const allPartialSegments = partialSegmentRefs.length > 0 &&
3349
+ !!hlsSegment.verbatimSegmentUri;
3350
+
3179
3351
  const reference = new shaka.media.SegmentReference(
3180
3352
  startTime,
3181
3353
  endTime,
3182
- getUris,
3354
+ getSegmentUris,
3183
3355
  startByte,
3184
3356
  endByte,
3185
3357
  initSegmentReference,
@@ -3192,6 +3364,7 @@ shaka.hls.HlsParser = class {
3192
3364
  syncTime,
3193
3365
  status,
3194
3366
  aes128Key,
3367
+ allPartialSegments,
3195
3368
  );
3196
3369
 
3197
3370
  if (segmentWithByteRangeOptimization) {
@@ -3239,18 +3412,17 @@ shaka.hls.HlsParser = class {
3239
3412
  * get the bandwidth necessary for this segments If it's defined in the
3240
3413
  * playlist.
3241
3414
  *
3242
- * @param {string} verbatimMediaPlaylistUri
3243
3415
  * @param {!shaka.hls.Playlist} playlist
3244
- * @param {string} type
3245
- * @param {string} mimeType
3416
+ * @param {shaka.extern.Stream} stream
3246
3417
  * @param {!Map.<number, number>} mediaSequenceToStartTime
3247
3418
  * @param {!Map.<string, string>} variables
3419
+ * @param {function():!Array.<string>} getUris
3248
3420
  * @return {{segments: !Array.<!shaka.media.SegmentReference>,
3249
3421
  * bandwidth: (number|undefined)}}
3250
3422
  * @private
3251
3423
  */
3252
- createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
3253
- mediaSequenceToStartTime, variables) {
3424
+ createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
3425
+ getUris) {
3254
3426
  /** @type {Array.<!shaka.hls.Segment>} */
3255
3427
  const hlsSegments = playlist.segments;
3256
3428
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -3302,7 +3474,8 @@ shaka.hls.HlsParser = class {
3302
3474
  for (const drmTag of item.tags) {
3303
3475
  if (drmTag.name == 'EXT-X-KEY') {
3304
3476
  if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
3305
- aes128Key = this.parseAES128DrmTag_(drmTag, playlist);
3477
+ aes128Key =
3478
+ this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
3306
3479
  } else {
3307
3480
  aes128Key = undefined;
3308
3481
  }
@@ -3312,7 +3485,7 @@ shaka.hls.HlsParser = class {
3312
3485
  mediaSequenceToStartTime.set(position, startTime);
3313
3486
 
3314
3487
  initSegmentRef = this.getInitSegmentReference_(playlist,
3315
- item.tags, variables);
3488
+ item.tags, getUris, variables);
3316
3489
 
3317
3490
  // If the stream is low latency and the user has not configured the
3318
3491
  // lowLatencyMode, but if it has been configured to activate the
@@ -3332,9 +3505,9 @@ shaka.hls.HlsParser = class {
3332
3505
  item,
3333
3506
  startTime,
3334
3507
  variables,
3335
- playlist.absoluteUri,
3336
- type,
3337
- mimeType,
3508
+ playlist,
3509
+ stream,
3510
+ getUris,
3338
3511
  aes128Key);
3339
3512
 
3340
3513
  if (reference) {
@@ -3468,42 +3641,6 @@ shaka.hls.HlsParser = class {
3468
3641
  };
3469
3642
  }
3470
3643
 
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
3644
  /**
3508
3645
  * Attempts to guess stream's mime type based on content type and URI.
3509
3646
  *
@@ -3537,6 +3674,10 @@ shaka.hls.HlsParser = class {
3537
3674
  }
3538
3675
  }
3539
3676
 
3677
+ if (codecs == 'mjpg') {
3678
+ return 'application/mp4';
3679
+ }
3680
+
3540
3681
  // Not enough information to guess from the content type and codecs.
3541
3682
  return null;
3542
3683
  }
@@ -3569,10 +3710,11 @@ shaka.hls.HlsParser = class {
3569
3710
  * @param {string} codecs
3570
3711
  * @param {!shaka.hls.Playlist} playlist
3571
3712
  * @param {!Map.<string, string>} variables
3713
+ * @param {function():!Array.<string>} getUris
3572
3714
  * @return {!Promise.<string>}
3573
3715
  * @private
3574
3716
  */
3575
- async guessMimeType_(contentType, codecs, playlist, variables) {
3717
+ async guessMimeType_(contentType, codecs, playlist, variables, getUris) {
3576
3718
  const HlsParser = shaka.hls.HlsParser;
3577
3719
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
3578
3720
 
@@ -3582,10 +3724,13 @@ shaka.hls.HlsParser = class {
3582
3724
  goog.asserts.assert(playlist.segments.length,
3583
3725
  'Playlist should have segments!');
3584
3726
  const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
3585
- const middleSegmentUri = this.variableSubstitution_(
3586
- playlist.segments[middleSegmentIdx].absoluteUri, variables);
3587
3727
 
3588
- const parsedUri = new goog.Uri(middleSegmentUri);
3728
+ const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
3729
+ getUris(),
3730
+ playlist.segments[middleSegmentIdx].verbatimSegmentUri,
3731
+ variables);
3732
+
3733
+ const parsedUri = new goog.Uri(middleSegmentUris[0]);
3589
3734
  const extension = parsedUri.getPath().split('.').pop();
3590
3735
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
3591
3736
 
@@ -3608,7 +3753,7 @@ shaka.hls.HlsParser = class {
3608
3753
  // If unable to guess mime type, request a segment and try getting it
3609
3754
  // from the response.
3610
3755
  const headRequest = shaka.net.NetworkingEngine.makeRequest(
3611
- [middleSegmentUri], this.config_.retryParameters);
3756
+ middleSegmentUris, this.config_.retryParameters);
3612
3757
  headRequest.method = 'HEAD';
3613
3758
  const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
3614
3759
  const response = await this.makeNetworkRequest_(
@@ -3650,14 +3795,17 @@ shaka.hls.HlsParser = class {
3650
3795
  * @param {?string} height
3651
3796
  * @param {?string} frameRate
3652
3797
  * @param {?string} videoRange
3798
+ * @param {?string} videoLayout
3653
3799
  * @private
3654
3800
  */
3655
- addVideoAttributes_(stream, width, height, frameRate, videoRange) {
3801
+ addVideoAttributes_(stream, width, height, frameRate, videoRange,
3802
+ videoLayout) {
3656
3803
  if (stream) {
3657
3804
  stream.width = Number(width) || undefined;
3658
3805
  stream.height = Number(height) || undefined;
3659
3806
  stream.frameRate = Number(frameRate) || undefined;
3660
3807
  stream.hdr = videoRange || undefined;
3808
+ stream.videoLayout = videoLayout || undefined;
3661
3809
  }
3662
3810
  }
3663
3811
 
@@ -3665,16 +3813,16 @@ shaka.hls.HlsParser = class {
3665
3813
  * Makes a network request for the manifest and returns a Promise
3666
3814
  * with the resulting data.
3667
3815
  *
3668
- * @param {string} absoluteUri
3816
+ * @param {!Array.<string>} uris
3669
3817
  * @param {boolean=} isPlaylist
3670
3818
  * @return {!Promise.<!shaka.extern.Response>}
3671
3819
  * @private
3672
3820
  */
3673
- requestManifest_(absoluteUri, isPlaylist) {
3821
+ requestManifest_(uris, isPlaylist) {
3674
3822
  const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
3675
3823
 
3676
3824
  const request = shaka.net.NetworkingEngine.makeRequest(
3677
- [absoluteUri], this.config_.retryParameters);
3825
+ uris, this.config_.retryParameters);
3678
3826
 
3679
3827
  const type = isPlaylist ?
3680
3828
  shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
@@ -3960,8 +4108,8 @@ shaka.hls.HlsParser = class {
3960
4108
  * @typedef {{
3961
4109
  * stream: !shaka.extern.Stream,
3962
4110
  * type: string,
3963
- * verbatimMediaPlaylistUri: string,
3964
- * absoluteMediaPlaylistUri: string,
4111
+ * redirectUris: !Array.<string>,
4112
+ * getUris: function():!Array.<string>,
3965
4113
  * minTimestamp: number,
3966
4114
  * maxTimestamp: number,
3967
4115
  * mediaSequenceToStartTime: !Map.<number, number>,
@@ -3981,14 +4129,10 @@ shaka.hls.HlsParser = class {
3981
4129
  * The Stream itself.
3982
4130
  * @property {string} type
3983
4131
  * 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.
4132
+ * @property {!Array.<string>} redirectUris
4133
+ * The redirect URIs.
4134
+ * @property {function():!Array.<string>} getUris
4135
+ * The verbatim media playlist URIs, as it appeared in the master playlist.
3992
4136
  * @property {number} minTimestamp
3993
4137
  * The minimum timestamp found in the stream.
3994
4138
  * @property {number} maxTimestamp
@@ -4030,30 +4174,6 @@ shaka.hls.HlsParser.StreamInfo;
4030
4174
  shaka.hls.HlsParser.StreamInfos;
4031
4175
 
4032
4176
 
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
4177
  /**
4058
4178
  * @const {!Object.<string, string>}
4059
4179
  * @private
@@ -4175,8 +4295,6 @@ shaka.hls.HlsParser.PresentationType_ = {
4175
4295
  LIVE: 'LIVE',
4176
4296
  };
4177
4297
 
4178
- shaka.media.ManifestParser.registerParserByExtension(
4179
- 'm3u8', () => new shaka.hls.HlsParser());
4180
4298
  shaka.media.ManifestParser.registerParserByMime(
4181
4299
  'application/x-mpegurl', () => new shaka.hls.HlsParser());
4182
4300
  shaka.media.ManifestParser.registerParserByMime(