shaka-player 4.4.3 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/.github/workflows/release-please.yaml +2 -27
  3. package/AUTHORS +2 -0
  4. package/CHANGELOG.md +156 -39
  5. package/CODE_OF_CONDUCT.md +1 -1
  6. package/CONTRIBUTORS +2 -0
  7. package/README.md +54 -42
  8. package/demo/asset_card.js +18 -21
  9. package/demo/cast_receiver/index.html +0 -1
  10. package/demo/close_button.js +0 -3
  11. package/demo/common/asset.js +33 -22
  12. package/demo/common/assets.js +249 -60
  13. package/demo/config.js +219 -253
  14. package/demo/custom.js +88 -133
  15. package/demo/front.js +7 -10
  16. package/demo/index.html +19 -22
  17. package/demo/input.js +6 -11
  18. package/demo/input_container.js +8 -9
  19. package/demo/main.js +62 -126
  20. package/demo/search.js +29 -34
  21. package/demo/service_worker.js +0 -3
  22. package/demo/tooltip.js +3 -5
  23. package/demo/visualizer.js +3 -5
  24. package/demo/visualizer_button.js +1 -20
  25. package/dist/controls.css +1 -1
  26. package/dist/controls.css.map +1 -1
  27. package/dist/demo.compiled.debug.js +211 -228
  28. package/dist/demo.compiled.debug.map +4 -4
  29. package/dist/demo.compiled.js +211 -228
  30. package/dist/demo.compiled.map +4 -4
  31. package/dist/deps.js +42 -37
  32. package/dist/locales.js +34 -30
  33. package/dist/receiver.compiled.debug.js +64 -77
  34. package/dist/receiver.compiled.debug.map +4 -4
  35. package/dist/receiver.compiled.js +64 -77
  36. package/dist/receiver.compiled.map +4 -4
  37. package/dist/shaka-player.compiled.d.ts +343 -151
  38. package/dist/shaka-player.compiled.debug.d.ts +343 -151
  39. package/dist/shaka-player.compiled.debug.externs.js +356 -117
  40. package/dist/shaka-player.compiled.debug.js +952 -811
  41. package/dist/shaka-player.compiled.debug.map +4 -4
  42. package/dist/shaka-player.compiled.externs.js +356 -117
  43. package/dist/shaka-player.compiled.js +1165 -1078
  44. package/dist/shaka-player.compiled.map +4 -4
  45. package/dist/shaka-player.ui.d.ts +359 -153
  46. package/dist/shaka-player.ui.debug.d.ts +359 -153
  47. package/dist/shaka-player.ui.debug.externs.js +382 -117
  48. package/dist/shaka-player.ui.debug.js +1084 -918
  49. package/dist/shaka-player.ui.debug.map +4 -4
  50. package/dist/shaka-player.ui.externs.js +382 -117
  51. package/dist/shaka-player.ui.js +1377 -1274
  52. package/dist/shaka-player.ui.map +4 -4
  53. package/docs/design/lcevc-integration.md +14 -14
  54. package/docs/tutorials/ad_monetization.md +54 -0
  55. package/docs/tutorials/basic-usage.md +2 -1
  56. package/docs/tutorials/errors.md +2 -1
  57. package/docs/tutorials/faq.md +27 -55
  58. package/docs/tutorials/index.json +2 -1
  59. package/docs/tutorials/lcevc.md +118 -0
  60. package/docs/tutorials/offline.md +10 -8
  61. package/docs/tutorials/plugins.md +5 -0
  62. package/docs/tutorials/ui-customization.md +17 -0
  63. package/docs/tutorials/ui.md +2 -1
  64. package/docs/tutorials/upgrade.md +3 -0
  65. package/externs/css_style_declaration.js +19 -0
  66. package/externs/lcevc.js +37 -13
  67. package/externs/managedmediasource.js +24 -0
  68. package/externs/media_remote.js +68 -0
  69. package/externs/mediatailor.js +193 -0
  70. package/externs/pictureinpicture.js +11 -0
  71. package/externs/shaka/abr_manager.js +6 -2
  72. package/externs/shaka/ads.js +27 -0
  73. package/externs/shaka/cea.js +7 -0
  74. package/externs/shaka/codecs.js +5 -3
  75. package/externs/shaka/manifest.js +31 -10
  76. package/externs/shaka/manifest_parser.js +24 -1
  77. package/externs/shaka/net.js +2 -2
  78. package/externs/shaka/offline.js +13 -2
  79. package/externs/shaka/player.js +65 -8
  80. package/externs/shaka/text.js +3 -1
  81. package/externs/shaka/transmuxer.js +1 -1
  82. package/karma.conf.js +7 -3
  83. package/lib/abr/simple_abr_manager.js +66 -9
  84. package/lib/ads/ad_manager.js +104 -50
  85. package/lib/ads/media_tailor_ad.js +297 -0
  86. package/lib/ads/media_tailor_ad_manager.js +839 -0
  87. package/lib/cast/cast_receiver.js +1 -1
  88. package/lib/cea/cea_decoder.js +28 -0
  89. package/lib/cea/dummy_caption_decoder.js +5 -0
  90. package/lib/cea/ts_cea_parser.js +15 -2
  91. package/lib/config/codec_switching_strategy.js +24 -0
  92. package/lib/dash/content_protection.js +120 -2
  93. package/lib/dash/dash_parser.js +322 -93
  94. package/lib/dash/mpd_utils.js +62 -37
  95. package/lib/dash/segment_base.js +17 -10
  96. package/lib/dash/segment_list.js +18 -9
  97. package/lib/dash/segment_template.js +122 -107
  98. package/lib/dependencies/all.js +8 -7
  99. package/lib/hls/hls_classes.js +28 -43
  100. package/lib/hls/hls_parser.js +798 -658
  101. package/lib/hls/hls_utils.js +64 -5
  102. package/lib/hls/manifest_text_parser.js +7 -11
  103. package/lib/lcevc/lcevc_dec.js +220 -0
  104. package/lib/media/adaptation_set.js +32 -14
  105. package/lib/media/adaptation_set_criteria.js +86 -16
  106. package/lib/media/closed_caption_parser.js +13 -0
  107. package/lib/media/content_workarounds.js +6 -0
  108. package/lib/media/drm_engine.js +8 -13
  109. package/lib/media/manifest_parser.js +8 -113
  110. package/lib/media/media_source_capabilities.js +10 -3
  111. package/lib/media/media_source_engine.js +286 -44
  112. package/lib/media/play_rate_controller.js +8 -38
  113. package/lib/media/presentation_timeline.js +7 -1
  114. package/lib/media/quality_observer.js +1 -1
  115. package/lib/media/segment_index.js +6 -6
  116. package/lib/media/segment_prefetch.js +92 -24
  117. package/lib/media/segment_reference.js +73 -7
  118. package/lib/media/segment_utils.js +343 -0
  119. package/lib/media/stall_detector.js +0 -3
  120. package/lib/media/streaming_engine.js +73 -38
  121. package/lib/mss/mss_parser.js +19 -2
  122. package/lib/net/networking_engine.js +67 -12
  123. package/lib/net/networking_utils.js +95 -0
  124. package/lib/offline/indexeddb/v1_storage_cell.js +3 -0
  125. package/lib/offline/indexeddb/v2_storage_cell.js +3 -0
  126. package/lib/offline/manifest_converter.js +3 -0
  127. package/lib/offline/offline_manifest_parser.js +10 -0
  128. package/lib/offline/storage.js +14 -7
  129. package/lib/player.js +1098 -1537
  130. package/lib/polyfill/mediasource.js +22 -1
  131. package/lib/text/cue.js +15 -0
  132. package/lib/text/mp4_ttml_parser.js +2 -2
  133. package/lib/text/srt_text_parser.js +2 -2
  134. package/lib/text/stub_text_displayer.js +51 -0
  135. package/lib/text/text_engine.js +3 -2
  136. package/lib/text/ttml_text_parser.js +45 -5
  137. package/lib/text/ui_text_displayer.js +8 -0
  138. package/lib/text/vtt_text_parser.js +40 -22
  139. package/lib/transmuxer/aac_transmuxer.js +1 -1
  140. package/lib/transmuxer/ac3_transmuxer.js +1 -1
  141. package/lib/transmuxer/ec3_transmuxer.js +1 -1
  142. package/lib/transmuxer/h264.js +5 -4
  143. package/lib/transmuxer/h265.js +863 -0
  144. package/lib/transmuxer/mp3_transmuxer.js +1 -1
  145. package/lib/transmuxer/mpeg_ts_transmuxer.js +4 -3
  146. package/lib/transmuxer/mss_transmuxer.js +1 -1
  147. package/lib/transmuxer/ts_transmuxer.js +187 -22
  148. package/lib/util/buffer_utils.js +26 -4
  149. package/lib/util/content_steering_manager.js +358 -0
  150. package/lib/util/error.js +30 -15
  151. package/lib/util/exp_golomb.js +30 -2
  152. package/lib/util/fairplay_utils.js +1 -1
  153. package/lib/util/fake_event.js +8 -1
  154. package/lib/util/id3_utils.js +50 -1
  155. package/lib/util/manifest_parser_utils.js +12 -8
  156. package/lib/util/mp4_generator.js +6 -8
  157. package/lib/util/mutex.js +59 -0
  158. package/lib/util/object_utils.js +1 -1
  159. package/lib/util/periods.js +156 -179
  160. package/lib/util/platform.js +44 -3
  161. package/lib/util/player_configuration.js +43 -6
  162. package/lib/util/state_history.js +6 -2
  163. package/lib/util/stream_utils.js +206 -283
  164. package/lib/util/ts_parser.js +335 -98
  165. package/lib/util/uint8array_utils.js +5 -1
  166. package/lib/util/xml_utils.js +5 -0
  167. package/maintained-branches.md +3 -2
  168. package/package.json +4 -6
  169. package/roadmap.md +25 -5
  170. package/shaka-player.uncompiled.js +2 -2
  171. package/support.html +0 -1
  172. package/test/abr/simple_abr_manager_unit.js +33 -33
  173. package/test/ads/ad_manager_unit.js +3 -2
  174. package/test/cast/cast_receiver_integration.js +50 -4
  175. package/test/cast/cast_utils_unit.js +2 -0
  176. package/test/cea/cea_decoder_unit.js +7 -0
  177. package/test/codec_switching/codec_switching_integration.js +182 -0
  178. package/test/dash/dash_parser_content_protection_unit.js +2 -0
  179. package/test/dash/dash_parser_integration.js +74 -0
  180. package/test/dash/dash_parser_live_unit.js +2 -19
  181. package/test/dash/dash_parser_manifest_unit.js +536 -2
  182. package/test/dash/dash_parser_segment_base_unit.js +2 -0
  183. package/test/dash/dash_parser_segment_list_unit.js +2 -0
  184. package/test/dash/dash_parser_segment_template_unit.js +2 -0
  185. package/test/dash/mpd_utils_unit.js +61 -30
  186. package/test/demo/demo_unit.js +0 -16
  187. package/test/hls/hls_live_unit.js +14 -3
  188. package/test/hls/hls_parser_unit.js +385 -33
  189. package/test/hls/manifest_text_parser_unit.js +32 -99
  190. package/test/media/adaptation_set_criteria_unit.js +284 -15
  191. package/test/media/adaptation_set_unit.js +2 -0
  192. package/test/media/content_workarounds_unit.js +1 -1
  193. package/test/media/drm_engine_unit.js +3 -3
  194. package/test/media/media_source_engine_unit.js +166 -2
  195. package/test/media/play_rate_controller_unit.js +0 -48
  196. package/test/media/segment_index_unit.js +5 -4
  197. package/test/media/segment_prefetch_unit.js +1 -1
  198. package/test/media/streaming_engine_unit.js +17 -17
  199. package/test/mss/mss_parser_unit.js +2 -0
  200. package/test/mss/mss_player_integration.js +3 -2
  201. package/test/net/networking_engine_unit.js +3 -3
  202. package/test/{media/manifest_parser_unit.js → net/networking_utils_unit.js} +2 -2
  203. package/test/offline/manifest_convert_unit.js +14 -0
  204. package/test/offline/offline_integration.js +2 -1
  205. package/test/offline/storage_integration.js +22 -13
  206. package/test/player_external.js +4 -2
  207. package/test/player_integration.js +15 -4
  208. package/test/player_load_graph_integration.js +47 -151
  209. package/test/player_src_equals_external.js +1 -0
  210. package/test/player_src_equals_integration.js +71 -4
  211. package/test/player_unit.js +182 -158
  212. package/test/test/assets/dash-aes-128/Fragments(video=0,format=mpd-time-csf) +0 -0
  213. package/test/test/assets/dash-aes-128/Fragments(video=20000000,format=mpd-time-csf) +0 -0
  214. package/test/test/assets/dash-aes-128/Fragments(video=40000000,format=mpd-time-csf) +0 -0
  215. package/test/test/assets/dash-aes-128/Fragments(video=60000000,format=mpd-time-csf) +0 -0
  216. package/test/test/assets/dash-aes-128/Fragments(video=i,format=mpd-time-csf) +0 -0
  217. package/test/test/assets/dash-aes-128/dash.mpd +18 -0
  218. package/test/test/assets/dash-aes-128/license +1 -0
  219. package/test/test/assets/dash-multi-codec/audio_en_2c_128k_aac.mp4 +0 -0
  220. package/test/test/assets/dash-multi-codec/audio_es_2c_64k_opus.webm +0 -0
  221. package/test/test/assets/dash-multi-codec/dash.mpd +32 -0
  222. package/test/test/assets/dash-multi-codec/video_144p_108k_h264.mp4 +0 -0
  223. package/test/test/assets/hls-ts-h265/0.ts +0 -0
  224. package/test/test/assets/hls-ts-h265/hevc.m3u8 +6 -0
  225. package/test/test/assets/hls-ts-h265/index.m3u8 +7 -0
  226. package/test/test/assets/hls-ts-muxed-aac-h265/0.ts +0 -0
  227. package/test/test/assets/hls-ts-muxed-aac-h265/1.ts +0 -0
  228. package/test/test/assets/hls-ts-muxed-aac-h265/media.m3u8 +9 -0
  229. package/test/test/assets/hls-ts-muxed-ac3-h264/media.m3u8 +24 -0
  230. package/test/test/assets/hls-ts-muxed-ac3-h264/s_00000.ts +0 -0
  231. package/test/test/assets/hls-ts-muxed-ac3-h264/s_00001.ts +0 -0
  232. package/test/test/assets/hls-ts-muxed-ac3-h264/s_00002.ts +0 -0
  233. package/test/test/assets/hls-ts-muxed-ac3-h264/s_00003.ts +0 -0
  234. package/test/test/assets/hls-ts-muxed-ac3-h264/s_00004.ts +0 -0
  235. package/test/test/assets/hls-ts-muxed-ac3-h264/s_00005.ts +0 -0
  236. package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0000.ts +0 -0
  237. package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0001.ts +0 -0
  238. package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0002.ts +0 -0
  239. package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0003.ts +0 -0
  240. package/test/test/assets/hls-ts-muxed-ec3-h264/prog_index.m3u8 +14 -0
  241. package/test/test/assets/hls-ts-muxed-mp3-h264/index.m3u8 +23 -0
  242. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-1-v1-a1.ts +0 -0
  243. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-2-v1-a1.ts +0 -0
  244. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-3-v1-a1.ts +0 -0
  245. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-4-v1-a1.ts +0 -0
  246. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-5-v1-a1.ts +0 -0
  247. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-6-v1-a1.ts +0 -0
  248. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-7-v1-a1.ts +0 -0
  249. package/test/test/assets/hls-ts-muxed-mp3-h264/seg-8-v1-a1.ts +0 -0
  250. package/test/test/util/dash_parser_util.js +4 -0
  251. package/test/test/util/fake_ad_manager.js +20 -0
  252. package/test/test/util/fake_media_source_engine.js +13 -2
  253. package/test/test/util/fake_networking_engine.js +9 -1
  254. package/test/test/util/fake_segment_prefetch.js +3 -0
  255. package/test/test/util/manifest_generator.js +11 -3
  256. package/test/test/util/manifest_parser_util.js +14 -2
  257. package/test/test/util/mss_parser_util.js +4 -0
  258. package/test/test/util/offline_utils.js +2 -0
  259. package/test/test/util/simple_fakes.js +2 -2
  260. package/test/test/util/streaming_engine_util.js +10 -4
  261. package/test/test/util/test_scheme.js +6 -0
  262. package/test/test/util/ui_utils.js +4 -3
  263. package/test/text/mp4_ttml_parser_unit.js +4 -4
  264. package/test/text/srt_text_parser_unit.js +1 -1
  265. package/test/text/text_displayer_layout_unit.js +1 -1
  266. package/test/text/text_engine_unit.js +6 -0
  267. package/test/text/ttml_text_parser_unit.js +85 -2
  268. package/test/text/vtt_text_parser_unit.js +103 -31
  269. package/test/transmuxer/transmuxer_engine_integration.js +9 -0
  270. package/test/transmuxer/transmuxer_integration.js +150 -3
  271. package/test/ui/ad_ui_unit.js +2 -2
  272. package/test/ui/ui_customization_unit.js +17 -16
  273. package/test/ui/ui_integration.js +30 -20
  274. package/test/ui/ui_unit.js +28 -29
  275. package/test/util/buffer_utils_unit.js +1 -1
  276. package/test/util/content_steering_manager_unit.js +427 -0
  277. package/test/util/id3_utils_unit.js +52 -0
  278. package/test/util/stream_utils_unit.js +207 -169
  279. package/test/util/ts_parser_unit.js +3 -3
  280. package/third_party/closure-uri/uri.js +13 -1
  281. package/ui/audio_language_selection.js +9 -2
  282. package/ui/controls.js +125 -0
  283. package/ui/externs/ui.js +24 -2
  284. package/ui/language_utils.js +43 -8
  285. package/ui/less/range_elements.less +33 -0
  286. package/ui/locales/en.json +1 -0
  287. package/ui/locales/es-419.json +1 -0
  288. package/ui/locales/es.json +1 -0
  289. package/ui/locales/source.json +4 -0
  290. package/ui/localization.js +10 -0
  291. package/ui/pip_button.js +22 -120
  292. package/ui/remote_button.js +203 -0
  293. package/ui/resolution_selection.js +47 -10
  294. package/ui/seek_bar.js +296 -3
  295. package/ui/text_selection.js +2 -1
  296. package/ui/ui.js +11 -4
  297. package/demo/common/message_ids.js +0 -309
  298. package/demo/locales/en.json +0 -288
  299. package/demo/locales/source.json +0 -1146
  300. package/externs/mux.js +0 -69
  301. package/lib/lcevc/lcevc_dil.js +0 -152
  302. package/lib/routing/node.js +0 -23
  303. package/lib/routing/payload.js +0 -43
  304. package/lib/routing/walker.js +0 -587
  305. package/lib/transmuxer/muxjs_transmuxer.js +0 -242
  306. package/test/routing/walker_unit.js +0 -504
  307. package/test/transmuxer/muxjs_transmuxer_integration.js +0 -177
@@ -9,6 +9,7 @@ goog.provide('shaka.hls.HlsParser');
9
9
 
10
10
  goog.require('goog.Uri');
11
11
  goog.require('goog.asserts');
12
+ goog.require('shaka.abr.Ewma');
12
13
  goog.require('shaka.hls.ManifestTextParser');
13
14
  goog.require('shaka.hls.Playlist');
14
15
  goog.require('shaka.hls.PlaylistType');
@@ -25,18 +26,16 @@ goog.require('shaka.net.DataUriPlugin');
25
26
  goog.require('shaka.net.NetworkingEngine');
26
27
  goog.require('shaka.util.ArrayUtils');
27
28
  goog.require('shaka.util.BufferUtils');
29
+ goog.require('shaka.util.ContentSteeringManager');
28
30
  goog.require('shaka.util.Error');
29
31
  goog.require('shaka.util.FakeEvent');
30
- goog.require('shaka.util.Functional');
31
32
  goog.require('shaka.util.LanguageUtils');
32
33
  goog.require('shaka.util.ManifestParserUtils');
33
34
  goog.require('shaka.util.MimeUtils');
34
- goog.require('shaka.util.Mp4BoxParsers');
35
- goog.require('shaka.util.Mp4Parser');
36
35
  goog.require('shaka.util.OperationManager');
37
36
  goog.require('shaka.util.Pssh');
37
+ goog.require('shaka.media.SegmentUtils');
38
38
  goog.require('shaka.util.Timer');
39
- goog.require('shaka.util.TsParser');
40
39
  goog.require('shaka.util.Platform');
41
40
  goog.require('shaka.util.Uint8ArrayUtils');
42
41
  goog.require('shaka.util.XmlUtils');
@@ -111,8 +110,8 @@ shaka.hls.HlsParser = class {
111
110
  * timestamps, offsets, and to handle TS rollover.
112
111
  *
113
112
  * During parsing, used to avoid duplicates in the async methods
114
- * createStreamInfoFromMediaTag_, createStreamInfoFromImageTag_ and
115
- * createStreamInfoFromVariantTag_.
113
+ * createStreamInfoFromMediaTags_, createStreamInfoFromImageTag_ and
114
+ * createStreamInfoFromVariantTags_.
116
115
  *
117
116
  * @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
118
117
  */
@@ -156,6 +155,13 @@ shaka.hls.HlsParser = class {
156
155
  */
157
156
  this.streamsFinalized_ = false;
158
157
 
158
+ /**
159
+ * Whether the manifest informs about the codec to use.
160
+ *
161
+ * @private
162
+ */
163
+ this.codecInfoInManifest_ = false;
164
+
159
165
  /**
160
166
  * This timer is used to trigger the start of a manifest update. A manifest
161
167
  * update is async. Once the update is finished, the timer will be restarted
@@ -215,6 +221,16 @@ shaka.hls.HlsParser = class {
215
221
 
216
222
  /** @private {boolean} */
217
223
  this.lowLatencyByterangeOptimization_ = false;
224
+
225
+ /**
226
+ * An ewma that tracks how long updates take.
227
+ * This is to mitigate issues caused by slow parsing on embedded devices.
228
+ * @private {!shaka.abr.Ewma}
229
+ */
230
+ this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
231
+
232
+ /** @private {?shaka.util.ContentSteeringManager} */
233
+ this.contentSteeringManager_ = null;
218
234
  }
219
235
 
220
236
 
@@ -224,6 +240,10 @@ shaka.hls.HlsParser = class {
224
240
  */
225
241
  configure(config) {
226
242
  this.config_ = config;
243
+
244
+ if (this.contentSteeringManager_) {
245
+ this.contentSteeringManager_.configure(this.config_);
246
+ }
227
247
  }
228
248
 
229
249
  /**
@@ -235,7 +255,7 @@ shaka.hls.HlsParser = class {
235
255
  this.playerInterface_ = playerInterface;
236
256
  this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
237
257
 
238
- const response = await this.requestManifest_(uri);
258
+ const response = await this.requestManifest_([uri]);
239
259
 
240
260
  // Record the master playlist URI after redirects.
241
261
  this.masterPlaylistUri_ = response.uri;
@@ -276,6 +296,10 @@ shaka.hls.HlsParser = class {
276
296
  this.groupIdToCodecsMap_.clear();
277
297
  this.globalVariables_.clear();
278
298
 
299
+ if (this.contentSteeringManager_) {
300
+ this.contentSteeringManager_.destroy();
301
+ }
302
+
279
303
  return Promise.all(pending);
280
304
  }
281
305
 
@@ -346,40 +370,42 @@ shaka.hls.HlsParser = class {
346
370
  * @private
347
371
  */
348
372
  async updateStream_(streamInfo) {
349
- const manifestUri = streamInfo.absoluteMediaPlaylistUri;
350
- const uriObj = new goog.Uri(manifestUri);
351
- const queryData = new goog.Uri.QueryData();
352
- if (streamInfo.canBlockReload) {
353
- if (streamInfo.nextMediaSequence >= 0) {
354
- // Indicates that the server must hold the request until a Playlist
355
- // contains a Media Segment with Media Sequence
356
- 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
+ }
357
389
  }
358
- if (streamInfo.nextPart >= 0) {
359
- // Indicates, in combination with _HLS_msn, that the server must hold
360
- // the request until a Playlist contains Partial Segment N of Media
361
- // Sequence Number M or later.
362
- 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');
363
394
  }
364
- }
365
- if (streamInfo.canSkipSegments) {
366
- // Enable delta updates. This will replace older segments with
367
- // 'EXT-X-SKIP' tag in the media playlist.
368
- queryData.add('_HLS_skip', 'YES');
369
- }
370
- if (queryData.getCount()) {
371
- uriObj.setQueryData(queryData);
395
+ if (queryData.getCount()) {
396
+ uriObj.setQueryData(queryData);
397
+ }
398
+ manifestUris.push(uriObj.toString());
372
399
  }
373
400
  const response =
374
- await this.requestManifest_(uriObj.toString(), /* isPlaylist= */ true);
401
+ await this.requestManifest_(manifestUris, /* isPlaylist= */ true);
375
402
  if (!streamInfo.stream.segmentIndex) {
376
403
  // The stream was closed since the update was first requested.
377
404
  return;
378
405
  }
379
406
 
380
407
  /** @type {shaka.hls.Playlist} */
381
- const playlist = this.manifestTextParser_.parsePlaylist(
382
- response.data, response.uri);
408
+ const playlist = this.manifestTextParser_.parsePlaylist(response.data);
383
409
 
384
410
  if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
385
411
  throw new shaka.util.Error(
@@ -388,11 +414,19 @@ shaka.hls.HlsParser = class {
388
414
  shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
389
415
  }
390
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
+
391
424
  /** @type {!Array.<!shaka.hls.Tag>} */
392
425
  const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
393
426
  'EXT-X-DEFINE');
394
427
 
395
- const mediaVariables = this.parseMediaVariables_(variablesTags);
428
+ const mediaVariables = this.parseMediaVariables_(
429
+ variablesTags, responseUri);
396
430
 
397
431
  const stream = streamInfo.stream;
398
432
 
@@ -410,8 +444,8 @@ shaka.hls.HlsParser = class {
410
444
  }
411
445
 
412
446
  const {segments, bandwidth} = this.createSegments_(
413
- streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
414
- stream.mimeType, mediaSequenceToStartTime, mediaVariables);
447
+ playlist, stream, mediaSequenceToStartTime, mediaVariables,
448
+ streamInfo.getUris);
415
449
 
416
450
  stream.bandwidth = bandwidth;
417
451
 
@@ -466,6 +500,24 @@ shaka.hls.HlsParser = class {
466
500
  // No-op
467
501
  }
468
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
+
469
521
  /**
470
522
  * Align the streams by sequence number by dropping early segments. Then
471
523
  * offset the streams to begin at presentation time 0.
@@ -591,7 +643,7 @@ shaka.hls.HlsParser = class {
591
643
  const segment0 = segmentIndex.earliestReference();
592
644
  if (segment0.syncTime == null) {
593
645
  shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
594
- streamInfo.verbatimMediaPlaylistUri,
646
+ streamInfo.getUris(),
595
647
  'Expect AV sync issues!');
596
648
  } else {
597
649
  // Stream metadata are offset by a fixed amount based on the
@@ -632,7 +684,7 @@ shaka.hls.HlsParser = class {
632
684
  }
633
685
 
634
686
  shaka.log.debug('Offset', offset, 'applied to',
635
- streamInfo.verbatimMediaPlaylistUri);
687
+ streamInfo.getUris());
636
688
  }
637
689
 
638
690
  /**
@@ -649,14 +701,11 @@ shaka.hls.HlsParser = class {
649
701
  goog.asserts.assert(this.masterPlaylistUri_,
650
702
  'Master playlist URI must be set before calling parseManifest_!');
651
703
 
652
- const playlist = this.manifestTextParser_.parsePlaylist(
653
- data, this.masterPlaylistUri_);
704
+ const playlist = this.manifestTextParser_.parsePlaylist(data);
654
705
 
655
706
  /** @type {!Array.<!shaka.hls.Tag>} */
656
707
  const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
657
708
 
658
- this.parseMasterVariables_(variablesTags);
659
-
660
709
  /** @type {!Array.<!shaka.extern.Variant>} */
661
710
  let variants = [];
662
711
  /** @type {!Array.<!shaka.extern.Stream>} */
@@ -666,9 +715,14 @@ shaka.hls.HlsParser = class {
666
715
 
667
716
  // Parsing a media playlist results in a single-variant stream.
668
717
  if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
718
+ const getUris = () => {
719
+ return [uri];
720
+ };
721
+
669
722
  // Get necessary info for this stream. These are things we would normally
670
723
  // find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
671
- const basicInfo = await this.getMediaPlaylistBasicInfo_(playlist);
724
+ const basicInfo =
725
+ await this.getMediaPlaylistBasicInfo_(playlist, getUris);
672
726
  const type = basicInfo.type;
673
727
  const mimeType = basicInfo.mimeType;
674
728
  const codecs = basicInfo.codecs;
@@ -677,6 +731,7 @@ shaka.hls.HlsParser = class {
677
731
  const width = basicInfo.width;
678
732
  const channelsCount = basicInfo.channelCount;
679
733
  const sampleRate = basicInfo.sampleRate;
734
+ const closedCaptions = basicInfo.closedCaptions;
680
735
 
681
736
  // Some values we cannot figure out, and aren't important enough to ask
682
737
  // the user to provide through config values. A lot of these are only
@@ -684,21 +739,21 @@ shaka.hls.HlsParser = class {
684
739
  // So these unknowns should be set to false or null, largely.
685
740
  const spatialAudio = false;
686
741
  const characteristics = null;
687
- const closedCaptions = new Map();
688
742
  const forced = false; // Only relevant for text.
689
743
  const primary = true; // This is the only stream!
690
744
  const name = 'Media Playlist';
691
745
 
692
746
  // Make the stream info, with those values.
693
747
  const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
694
- playlist, uri, uri, codecs, type, languageValue, primary, name,
695
- channelsCount, closedCaptions, characteristics, forced, sampleRate,
696
- spatialAudio, mimeType);
748
+ this.globalId_++, playlist, getUris, uri, codecs, type,
749
+ languageValue, primary, name, channelsCount, closedCaptions,
750
+ characteristics, forced, sampleRate, spatialAudio, mimeType);
697
751
  this.uriToStreamInfosMap_.set(uri, streamInfo);
698
752
 
699
753
  if (type == 'video') {
700
754
  this.addVideoAttributes_(streamInfo.stream, width, height,
701
- /* frameRate= */ null, /* videoRange= */ null);
755
+ /* frameRate= */ null, /* videoRange= */ null,
756
+ /* videoLayout= */ null);
702
757
  }
703
758
 
704
759
  // Wrap the stream from that stream info with a variant.
@@ -715,8 +770,11 @@ shaka.hls.HlsParser = class {
715
770
  decodingInfos: [],
716
771
  });
717
772
  } else {
773
+ this.parseMasterVariables_(variablesTags);
774
+
718
775
  /** @type {!Array.<!shaka.hls.Tag>} */
719
- const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
776
+ const mediaTags = Utils.filterTagsByName(
777
+ playlist.tags, 'EXT-X-MEDIA');
720
778
  /** @type {!Array.<!shaka.hls.Tag>} */
721
779
  const variantTags = Utils.filterTagsByName(
722
780
  playlist.tags, 'EXT-X-STREAM-INF');
@@ -724,43 +782,28 @@ shaka.hls.HlsParser = class {
724
782
  const imageTags = Utils.filterTagsByName(
725
783
  playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
726
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>} */
727
788
  const sessionKeyTags = Utils.filterTagsByName(
728
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');
729
796
 
730
- this.parseCodecs_(variantTags);
797
+ this.processSessionData_(sessionDataTags);
798
+ await this.processContentSteering_(contentSteeringTags);
731
799
 
732
- /** @type {!Array.<!shaka.hls.Tag>} */
733
- const sesionDataTags =
734
- Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA');
735
- for (const tag of sesionDataTags) {
736
- const id = tag.getAttributeValue('DATA-ID');
737
- const uri = tag.getAttributeValue('URI');
738
- const language = tag.getAttributeValue('LANGUAGE');
739
- const value = tag.getAttributeValue('VALUE');
740
- const data = (new Map()).set('id', id);
741
- if (uri) {
742
- data.set('uri', shaka.hls.Utils.constructAbsoluteUri(
743
- this.masterPlaylistUri_, uri));
744
- }
745
- if (language) {
746
- data.set('language', language);
747
- }
748
- if (value) {
749
- data.set('value', value);
750
- }
751
- const event = new shaka.util.FakeEvent('sessiondata', data);
752
- if (this.playerInterface_) {
753
- this.playerInterface_.onEvent(event);
754
- }
755
- }
800
+ this.parseCodecs_(variantTags);
756
801
 
757
- // Parse audio and video media tags first, so that we can extract segment
758
- // start time from audio/video streams and reuse for text streams.
759
- this.createStreamInfosFromMediaTags_(mediaTags);
760
802
  this.parseClosedCaptions_(mediaTags);
761
- variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
803
+ variants =
804
+ this.createVariantsForTags_(variantTags, sessionKeyTags, mediaTags);
762
805
  textStreams = this.parseTexts_(mediaTags);
763
- imageStreams = await this.parseImages_(imageTags);
806
+ imageStreams = await this.parseImages_(imageTags, iFrameTags);
764
807
  }
765
808
 
766
809
  // Make sure that the parser has not been destroyed.
@@ -809,64 +852,72 @@ shaka.hls.HlsParser = class {
809
852
  type: shaka.media.ManifestParser.HLS,
810
853
  serviceDescription: null,
811
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
+
812
873
  this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
813
874
  }
814
875
 
815
876
  /**
816
877
  * @param {shaka.hls.Playlist} playlist
817
- * @return {!Promise.<shaka.hls.HlsParser.BasicInfo>}
878
+ * @param {function():!Array.<string>} getUris
879
+ * @return {!Promise.<shaka.media.SegmentUtils.BasicInfo>}
818
880
  * @private
819
881
  */
820
- async getMediaPlaylistBasicInfo_(playlist) {
882
+ async getMediaPlaylistBasicInfo_(playlist, getUris) {
821
883
  const HlsParser = shaka.hls.HlsParser;
822
- const defaultFullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
823
- const defaultMimeType =
824
- shaka.util.MimeUtils.getBasicType(defaultFullMimeType);
825
- const defaultType = defaultMimeType.split('/')[0];
826
- const defaultCodecs = shaka.util.MimeUtils.getCodecs(defaultFullMimeType);
827
- const defaultBasicInfo = {
828
- type: defaultType,
829
- mimeType: defaultMimeType,
830
- codecs: defaultCodecs,
831
- language: null,
832
- height: null,
833
- width: null,
834
- channelCount: null,
835
- sampleRate: null,
836
- };
884
+ const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType(
885
+ this.config_.hls.mediaPlaylistFullMimeType);
837
886
  if (!playlist.segments.length) {
838
887
  return defaultBasicInfo;
839
888
  }
840
- const firstSegment = playlist.segments[0];
841
- 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);
842
896
  const extension = parsedUri.getPath().split('.').pop();
843
897
  const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
844
898
  if (rawMimeType) {
845
- return {
846
- type: 'audio',
847
- mimeType: rawMimeType,
848
- codecs: '',
849
- language: null,
850
- height: null,
851
- width: null,
852
- channelCount: null,
853
- sampleRate: null,
854
- };
899
+ return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
900
+ rawMimeType);
855
901
  }
856
902
 
857
- let segmentUris = [firstSegment.absoluteUri];
903
+ const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
904
+
905
+ let initData = null;
858
906
  const initSegmentRef = this.getInitSegmentReference_(
859
- playlist, firstSegment.tags, new Map());
907
+ playlist, middleSegment.tags, getUris);
860
908
  if (initSegmentRef) {
861
- 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;
862
916
  }
863
917
 
864
- const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
865
918
  const segmentRequest = shaka.net.NetworkingEngine.makeRequest(
866
- segmentUris, this.config_.retryParameters);
867
- const type = initSegmentRef ?
868
- shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT :
869
- shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
919
+ middleSegmentUris, this.config_.retryParameters);
920
+ const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
870
921
  const response = await this.makeNetworkRequest_(
871
922
  segmentRequest, requestType, {type});
872
923
 
@@ -877,13 +928,16 @@ shaka.hls.HlsParser = class {
877
928
  }
878
929
 
879
930
  if (extension == 'ts' || contentMimeType == 'video/mp2t') {
880
- const basicInfo = this.getBasicInfoFromTs_(response);
931
+ const basicInfo =
932
+ shaka.media.SegmentUtils.getBasicInfoFromTs(response.data);
881
933
  if (basicInfo) {
882
934
  return basicInfo;
883
935
  }
884
- } else if (extension == 'mp4' ||
885
- contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4') {
886
- 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);
887
941
  if (basicInfo) {
888
942
  return basicInfo;
889
943
  }
@@ -891,232 +945,6 @@ shaka.hls.HlsParser = class {
891
945
  return defaultBasicInfo;
892
946
  }
893
947
 
894
- /**
895
- * @param {shaka.extern.Response} response
896
- * @return {?shaka.hls.HlsParser.BasicInfo}
897
- * @private
898
- */
899
- getBasicInfoFromTs_(response) {
900
- const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data);
901
- const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
902
- const tsCodecs = tsParser.getCodecs();
903
- const videoInfo = tsParser.getVideoInfo();
904
- const codecs = [];
905
- let hasAudio = false;
906
- let hasVideo = false;
907
- switch (tsCodecs.audio) {
908
- case 'aac':
909
- codecs.push('mp4a.40.2');
910
- hasAudio = true;
911
- break;
912
- case 'mp3':
913
- codecs.push('mp4a.40.34');
914
- hasAudio = true;
915
- break;
916
- case 'ac3':
917
- codecs.push('ac-3');
918
- hasAudio = true;
919
- break;
920
- case 'ec3':
921
- codecs.push('ec-3');
922
- hasAudio = true;
923
- break;
924
- }
925
- switch (tsCodecs.video) {
926
- case 'avc':
927
- if (videoInfo.codec) {
928
- codecs.push(videoInfo.codec);
929
- } else {
930
- codecs.push('avc1.42E01E');
931
- }
932
- hasVideo = true;
933
- break;
934
- case 'hvc':
935
- codecs.push('hvc1.1.6.L93.90');
936
- hasVideo = true;
937
- break;
938
- }
939
- if (!codecs.length) {
940
- return null;
941
- }
942
- const onlyAudio = hasAudio && !hasVideo;
943
- return {
944
- type: onlyAudio ? 'audio' : 'video',
945
- mimeType: 'video/mp2t',
946
- codecs: codecs.join(', '),
947
- language: null,
948
- height: videoInfo.height,
949
- width: videoInfo.width,
950
- channelCount: null,
951
- sampleRate: null,
952
- };
953
- }
954
-
955
- /**
956
- * @param {shaka.extern.Response} response
957
- * @return {?shaka.hls.HlsParser.BasicInfo}
958
- * @private
959
- */
960
- getBasicInfoFromMp4_(response) {
961
- const Mp4Parser = shaka.util.Mp4Parser;
962
-
963
- const codecs = [];
964
-
965
- let hasAudio = false;
966
- let hasVideo = false;
967
-
968
- const addCodec = (codec) => {
969
- const codecLC = codec.toLowerCase();
970
- switch (codecLC) {
971
- case 'avc1':
972
- case 'avc3':
973
- codecs.push(codecLC + '.42E01E');
974
- hasVideo = true;
975
- break;
976
- case 'hev1':
977
- case 'hvc1':
978
- codecs.push(codecLC + '.1.6.L93.90');
979
- hasVideo = true;
980
- break;
981
- case 'dvh1':
982
- case 'dvhe':
983
- codecs.push(codecLC + '.05.04');
984
- hasVideo = true;
985
- break;
986
- case 'vp09':
987
- codecs.push(codecLC + '.00.10.08');
988
- hasVideo = true;
989
- break;
990
- case 'av01':
991
- codecs.push(codecLC + '.0.01M.08');
992
- hasVideo = true;
993
- break;
994
- case 'mp4a':
995
- // We assume AAC, but this can be wrong since mp4a supports
996
- // others codecs
997
- codecs.push('mp4a.40.2');
998
- hasAudio = true;
999
- break;
1000
- case 'ac-3':
1001
- case 'ec-3':
1002
- case 'opus':
1003
- case 'flac':
1004
- codecs.push(codecLC);
1005
- hasAudio = true;
1006
- break;
1007
- }
1008
- };
1009
-
1010
- const codecBoxParser = (box) => addCodec(box.name);
1011
-
1012
- /** @type {?string} */
1013
- let language = null;
1014
- /** @type {?string} */
1015
- let height = null;
1016
- /** @type {?string} */
1017
- let width = null;
1018
- /** @type {?number} */
1019
- let channelCount = null;
1020
- /** @type {?number} */
1021
- let sampleRate = null;
1022
-
1023
- new Mp4Parser()
1024
- .box('moov', Mp4Parser.children)
1025
- .box('trak', Mp4Parser.children)
1026
- .fullBox('tkhd', (box) => {
1027
- goog.asserts.assert(
1028
- box.version != null,
1029
- 'TKHD is a full box and should have a valid version.');
1030
- const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
1031
- box.reader, box.version);
1032
- height = String(parsedTKHDBox.height);
1033
- width = String(parsedTKHDBox.width);
1034
- })
1035
- .box('mdia', Mp4Parser.children)
1036
- .fullBox('mdhd', (box) => {
1037
- goog.asserts.assert(
1038
- box.version != null,
1039
- 'MDHD is a full box and should have a valid version.');
1040
- const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
1041
- box.reader, box.version);
1042
- language = parsedMDHDBox.language;
1043
- })
1044
- .box('minf', Mp4Parser.children)
1045
- .box('stbl', Mp4Parser.children)
1046
- .fullBox('stsd', Mp4Parser.sampleDescription)
1047
-
1048
- // AUDIO
1049
- // These are the various boxes that signal a codec.
1050
- .box('mp4a', (box) => {
1051
- const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader);
1052
- channelCount = parsedMP4ABox.channelCount;
1053
- sampleRate = parsedMP4ABox.sampleRate;
1054
- if (box.reader.hasMoreData()) {
1055
- Mp4Parser.children(box);
1056
- } else {
1057
- codecBoxParser(box);
1058
- }
1059
- })
1060
- .box('esds', (box) => {
1061
- const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader);
1062
- codecs.push(parsedESDSBox.codec);
1063
- hasAudio = true;
1064
- })
1065
- .box('ac-3', codecBoxParser)
1066
- .box('ec-3', codecBoxParser)
1067
- .box('opus', codecBoxParser)
1068
- .box('Opus', codecBoxParser)
1069
- .box('fLaC', codecBoxParser)
1070
-
1071
- // VIDEO
1072
- // These are the various boxes that signal a codec.
1073
- .box('avc1', (box) => {
1074
- const parsedAVCBox =
1075
- shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
1076
- codecs.push(parsedAVCBox.codec);
1077
- hasVideo = true;
1078
- })
1079
- .box('avc3', (box) => {
1080
- const parsedAVCBox =
1081
- shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
1082
- codecs.push(parsedAVCBox.codec);
1083
- hasVideo = true;
1084
- })
1085
- .box('hev1', codecBoxParser)
1086
- .box('hvc1', codecBoxParser)
1087
- .box('dvh1', codecBoxParser)
1088
- .box('dvhe', codecBoxParser)
1089
- .box('vp09', codecBoxParser)
1090
- .box('av01', codecBoxParser)
1091
-
1092
- // This signals an encrypted sample, which we can go inside of to
1093
- // find the codec used.
1094
- // Note: If encrypted, you can only have audio or video, not both.
1095
- .box('enca', Mp4Parser.visualSampleEntry)
1096
- .box('encv', Mp4Parser.visualSampleEntry)
1097
- .box('sinf', Mp4Parser.children)
1098
- .box('frma', (box) => {
1099
- const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
1100
- addCodec(codec);
1101
- })
1102
-
1103
- .parse(response.data, /* partialOkay= */ true);
1104
- if (!codecs.length) {
1105
- return null;
1106
- }
1107
- const onlyAudio = hasAudio && !hasVideo;
1108
- return {
1109
- type: onlyAudio ? 'audio' : 'video',
1110
- mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4',
1111
- codecs: this.filterDuplicateCodecs_(codecs).join(', '),
1112
- language: language,
1113
- height: height,
1114
- width: width,
1115
- channelCount: channelCount,
1116
- sampleRate: sampleRate,
1117
- };
1118
- }
1119
-
1120
948
  /** @private */
1121
949
  determineDuration_() {
1122
950
  goog.asserts.assert(this.presentationTimeline_,
@@ -1171,31 +999,50 @@ shaka.hls.HlsParser = class {
1171
999
  * @private
1172
1000
  */
1173
1001
  parseMasterVariables_(tags) {
1002
+ const queryParams = new goog.Uri(this.masterPlaylistUri_).getQueryData();
1174
1003
  for (const variableTag of tags) {
1175
1004
  const name = variableTag.getAttributeValue('NAME');
1176
1005
  const value = variableTag.getAttributeValue('VALUE');
1006
+ const queryParam = variableTag.getAttributeValue('QUERYPARAM');
1177
1007
  if (name && value) {
1178
1008
  if (!this.globalVariables_.has(name)) {
1179
1009
  this.globalVariables_.set(name, value);
1180
1010
  }
1181
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
+ }
1182
1018
  }
1183
1019
  }
1184
1020
 
1185
1021
  /**
1186
1022
  * Get the variables of each variant tag, and store in a map.
1187
1023
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
1024
+ * @param {string} uri Media playlist URI.
1188
1025
  * @return {!Map.<string, string>}
1189
1026
  * @private
1190
1027
  */
1191
- parseMediaVariables_(tags) {
1028
+ parseMediaVariables_(tags, uri) {
1029
+ const queryParams = new goog.Uri(uri).getQueryData();
1192
1030
  const mediaVariables = new Map();
1193
1031
  for (const variableTag of tags) {
1194
1032
  const name = variableTag.getAttributeValue('NAME');
1195
1033
  const value = variableTag.getAttributeValue('VALUE');
1034
+ const queryParam = variableTag.getAttributeValue('QUERYPARAM');
1196
1035
  const mediaImport = variableTag.getAttributeValue('IMPORT');
1197
1036
  if (name && value) {
1198
- 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
+ }
1199
1046
  }
1200
1047
  if (mediaImport) {
1201
1048
  const globalValue = this.globalVariables_.get(mediaImport);
@@ -1248,6 +1095,68 @@ shaka.hls.HlsParser = class {
1248
1095
  }
1249
1096
  }
1250
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
+
1251
1160
  /**
1252
1161
  * Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags.
1253
1162
  * Create text streams for Subtitles, but not Closed Captions.
@@ -1266,7 +1175,7 @@ shaka.hls.HlsParser = class {
1266
1175
  return null;
1267
1176
  }
1268
1177
  try {
1269
- return this.createStreamInfoFromMediaTag_(tag).stream;
1178
+ return this.createStreamInfoFromMediaTags_([tag], new Map()).stream;
1270
1179
  } catch (e) {
1271
1180
  if (this.config_.hls.ignoreTextStreamFailures) {
1272
1181
  return null;
@@ -1300,10 +1209,11 @@ shaka.hls.HlsParser = class {
1300
1209
 
1301
1210
  /**
1302
1211
  * @param {!Array.<!shaka.hls.Tag>} imageTags from the playlist.
1212
+ * @param {!Array.<!shaka.hls.Tag>} iFrameTags from the playlist.
1303
1213
  * @return {!Promise.<!Array.<!shaka.extern.Stream>>}
1304
1214
  * @private
1305
1215
  */
1306
- async parseImages_(imageTags) {
1216
+ async parseImages_(imageTags, iFrameTags) {
1307
1217
  // Create image stream for each image tag.
1308
1218
  const imageStreamPromises = imageTags.map(async (tag) => {
1309
1219
  const disableThumbnails = this.config_.disableThumbnails;
@@ -1319,16 +1229,34 @@ shaka.hls.HlsParser = class {
1319
1229
  }
1320
1230
  throw e;
1321
1231
  }
1322
- });
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
+ }));
1323
1250
  const imageStreams = await Promise.all(imageStreamPromises);
1324
1251
  return imageStreams.filter((s) => s);
1325
1252
  }
1326
1253
 
1327
1254
  /**
1328
1255
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
1256
+ * @param {!Map.<string, string>} groupIdPathwayIdMapping
1329
1257
  * @private
1330
1258
  */
1331
- createStreamInfosFromMediaTags_(mediaTags) {
1259
+ createStreamInfosFromMediaTags_(mediaTags, groupIdPathwayIdMapping) {
1332
1260
  // Filter out subtitles and media tags without uri.
1333
1261
  mediaTags = mediaTags.filter((tag) => {
1334
1262
  const uri = tag.getAttributeValue('URI') || '';
@@ -1336,9 +1264,20 @@ shaka.hls.HlsParser = class {
1336
1264
  return type != 'SUBTITLES' && uri != '';
1337
1265
  });
1338
1266
 
1339
- // Create stream info for each audio / video media tag.
1267
+ const groupedTags = {};
1340
1268
  for (const tag of mediaTags) {
1341
- 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);
1342
1281
  }
1343
1282
  }
1344
1283
 
@@ -1346,10 +1285,12 @@ shaka.hls.HlsParser = class {
1346
1285
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
1347
1286
  * @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
1348
1287
  * from the playlist.
1288
+ * @param {!Array.<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
1289
+ * playlist.
1349
1290
  * @return {!Array.<!shaka.extern.Variant>}
1350
1291
  * @private
1351
1292
  */
1352
- createVariantsForTags_(tags, sessionKeyTags) {
1293
+ createVariantsForTags_(tags, sessionKeyTags, mediaTags) {
1353
1294
  // EXT-X-SESSION-KEY processing
1354
1295
  const drmInfos = [];
1355
1296
  const keyIds = new Set();
@@ -1381,24 +1322,54 @@ shaka.hls.HlsParser = class {
1381
1322
  }
1382
1323
  }
1383
1324
 
1384
- // Create variants for each variant tag.
1385
- const allVariants = tags.map((tag) => {
1386
- const frameRate = tag.getAttributeValue('FRAME-RATE');
1387
- const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
1388
- 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
+ }
1389
1334
 
1390
- const resolution = tag.getAttributeValue('RESOLUTION');
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'));
1345
+
1346
+ const resolution = firstTag.getAttributeValue('RESOLUTION');
1391
1347
  const [width, height] = resolution ? resolution.split('x') : [null, null];
1392
1348
 
1393
- const videoRange = tag.getAttributeValue('VIDEO-RANGE');
1349
+ const videoRange = firstTag.getAttributeValue('VIDEO-RANGE');
1394
1350
 
1395
- const streamInfos = this.createStreamInfosForVariantTag_(tag,
1396
- 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);
1397
1368
 
1398
1369
  goog.asserts.assert(streamInfos.audio.length ||
1399
1370
  streamInfos.video.length, 'We should have created a stream!');
1400
1371
 
1401
- return this.createVariants_(
1372
+ allVariants.push(...this.createVariants_(
1402
1373
  streamInfos.audio,
1403
1374
  streamInfos.video,
1404
1375
  bandwidth,
@@ -1406,47 +1377,78 @@ shaka.hls.HlsParser = class {
1406
1377
  height,
1407
1378
  frameRate,
1408
1379
  videoRange,
1380
+ videoLayout,
1409
1381
  drmInfos,
1410
- keyIds);
1411
- });
1412
- let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
1413
- // Filter out null variants.
1414
- variants = variants.filter((variant) => variant != null);
1415
- return variants;
1382
+ keyIds));
1383
+ }
1384
+ return allVariants.filter((variant) => variant != null);
1416
1385
  }
1417
1386
 
1418
1387
  /**
1419
1388
  * Create audio and video streamInfos from an 'EXT-X-STREAM-INF' tag and its
1420
1389
  * related media tags.
1421
1390
  *
1422
- * @param {!shaka.hls.Tag} tag
1391
+ * @param {!Array.<!shaka.hls.Tag>} tags
1392
+ * @param {!Array.<!shaka.hls.Tag>} mediaTags
1423
1393
  * @param {?string} resolution
1424
1394
  * @param {?string} frameRate
1425
1395
  * @return {!shaka.hls.HlsParser.StreamInfos}
1426
1396
  * @private
1427
1397
  */
1428
- createStreamInfosForVariantTag_(tag, resolution, frameRate) {
1398
+ createStreamInfosForVariantTags_(tags, mediaTags, resolution, frameRate) {
1429
1399
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1430
- /** @type {!Array.<string>} */
1431
- let allCodecs = this.getCodecsForVariantTag_(tag);
1432
- const audioGroupId = tag.getAttributeValue('AUDIO');
1433
- const videoGroupId = tag.getAttributeValue('VIDEO');
1434
- goog.asserts.assert(audioGroupId == null || videoGroupId == null,
1435
- 'Unexpected: both video and audio described by media tags!');
1436
-
1437
- const groupId = audioGroupId || videoGroupId;
1438
- const streamInfos =
1439
- (groupId && this.groupIdToStreamInfosMap_.has(groupId)) ?
1440
- this.groupIdToStreamInfosMap_.get(groupId) : [];
1441
1400
 
1442
1401
  /** @type {shaka.hls.HlsParser.StreamInfos} */
1443
1402
  const res = {
1444
- audio: audioGroupId ? streamInfos : [],
1445
- video: videoGroupId ? streamInfos : [],
1403
+ audio: [],
1404
+ video: [],
1446
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
+ }
1447
1451
 
1448
- // Make an educated guess about the stream type.
1449
- shaka.log.debug('Guessing stream type for', tag.toString());
1450
1452
  let type;
1451
1453
  let ignoreStream = false;
1452
1454
 
@@ -1455,11 +1457,15 @@ shaka.hls.HlsParser = class {
1455
1457
  // In that case, ignore the local URI and use the version in the
1456
1458
  // AUDIO tag, so you inherit its language.
1457
1459
  // As an example, see the manifest linked in issue #860.
1458
- const streamURI = tag.getRequiredAttrValue('URI');
1460
+ const allStreamUris = tags.map((tag) => tag.getRequiredAttrValue('URI'));
1459
1461
  const hasSameUri = res.audio.find((audio) => {
1460
- return audio && audio.verbatimMediaPlaylistUri == streamURI;
1462
+ return audio && audio.getUris().find((uri) => {
1463
+ return allStreamUris.includes(uri);
1464
+ });
1461
1465
  });
1462
1466
 
1467
+ /** @type {!Array.<string>} */
1468
+ let allCodecs = this.getCodecsForVariantTag_(tags[0]);
1463
1469
  const videoCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
1464
1470
  ContentType.VIDEO, allCodecs);
1465
1471
  const audioCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
@@ -1471,7 +1477,8 @@ shaka.hls.HlsParser = class {
1471
1477
  type = ContentType.AUDIO;
1472
1478
  shaka.log.debug('Guessing audio-only.');
1473
1479
  ignoreStream = res.audio.length > 0;
1474
- } else if (!streamInfos.length && audioCodecs && videoCodecs) {
1480
+ } else if (!res.audio.length && !res.video.length &&
1481
+ audioCodecs && videoCodecs) {
1475
1482
  // There are both audio and video codecs, so assume multiplexed content.
1476
1483
  // Note that the default used when CODECS is missing assumes multiple
1477
1484
  // (and therefore multiplexed).
@@ -1494,8 +1501,35 @@ shaka.hls.HlsParser = class {
1494
1501
  }
1495
1502
 
1496
1503
  if (!ignoreStream) {
1497
- const streamInfo =
1498
- 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
+ }
1499
1533
  res[streamInfo.stream.type] = [streamInfo];
1500
1534
  }
1501
1535
  return res;
@@ -1510,17 +1544,24 @@ shaka.hls.HlsParser = class {
1510
1544
  * @private
1511
1545
  */
1512
1546
  getCodecsForVariantTag_(tag) {
1513
- // These are the default codecs to assume if none are specified.
1514
- const defaultCodecsArray = [];
1515
- if (!this.config_.disableVideo) {
1516
- defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
1517
- }
1518
- if (!this.config_.disableAudio) {
1519
- 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(',');
1520
1563
  }
1521
- const defaultCodecs = defaultCodecsArray.join(',');
1522
1564
 
1523
- const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
1524
1565
  // Strip out internal whitespace while splitting on commas:
1525
1566
  /** @type {!Array.<string>} */
1526
1567
  const codecs = codecsString.split(/\s*,\s*/);
@@ -1674,6 +1715,7 @@ shaka.hls.HlsParser = class {
1674
1715
  * @param {?string} height
1675
1716
  * @param {?string} frameRate
1676
1717
  * @param {?string} videoRange
1718
+ * @param {?string} videoLayout
1677
1719
  * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1678
1720
  * @param {!Set.<string>} keyIds
1679
1721
  * @return {!Array.<!shaka.extern.Variant>}
@@ -1681,13 +1723,13 @@ shaka.hls.HlsParser = class {
1681
1723
  */
1682
1724
  createVariants_(
1683
1725
  audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1684
- drmInfos, keyIds) {
1726
+ videoLayout, drmInfos, keyIds) {
1685
1727
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1686
1728
  const DrmEngine = shaka.media.DrmEngine;
1687
1729
 
1688
1730
  for (const info of videoInfos) {
1689
1731
  this.addVideoAttributes_(
1690
- info.stream, width, height, frameRate, videoRange);
1732
+ info.stream, width, height, frameRate, videoRange, videoLayout);
1691
1733
  }
1692
1734
 
1693
1735
  // In case of audio-only or video-only content or the audio/video is
@@ -1719,9 +1761,9 @@ shaka.hls.HlsParser = class {
1719
1761
  const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
1720
1762
  const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
1721
1763
  const videoStreamUri =
1722
- videoInfo ? videoInfo.verbatimMediaPlaylistUri : '';
1764
+ videoInfo ? videoInfo.getUris().sort().join(',') : '';
1723
1765
  const audioStreamUri =
1724
- audioInfo ? audioInfo.verbatimMediaPlaylistUri : '';
1766
+ audioInfo ? audioInfo.getUris().sort().join(',') : '';
1725
1767
  const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
1726
1768
 
1727
1769
  if (audioStream && videoStream) {
@@ -1809,34 +1851,59 @@ shaka.hls.HlsParser = class {
1809
1851
  /**
1810
1852
  * Parse EXT-X-MEDIA media tag into a Stream object.
1811
1853
  *
1812
- * @param {shaka.hls.Tag} tag
1854
+ * @param {!Array.<!shaka.hls.Tag>} tags
1855
+ * @param {!Map.<string, string>} groupIdPathwayIdMapping
1813
1856
  * @return {!shaka.hls.HlsParser.StreamInfo}
1814
1857
  * @private
1815
1858
  */
1816
- createStreamInfoFromMediaTag_(tag) {
1817
- goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1818
- 'Should only be called on media tags!');
1819
- const groupId = tag.getRequiredAttrValue('GROUP-ID');
1859
+ createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping) {
1860
+ const verbatimMediaPlaylistUris = [];
1861
+ const globalGroupIds = [];
1862
+ const groupIdUriMappping = new Map();
1863
+ for (const tag of tags) {
1864
+ goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1865
+ 'Should only be called on media tags!');
1866
+ const uri = tag.getRequiredAttrValue('URI');
1867
+ const groupId = tag.getRequiredAttrValue('GROUP-ID');
1868
+ verbatimMediaPlaylistUris.push(uri);
1869
+ globalGroupIds.push(groupId);
1870
+ groupIdUriMappping.set(groupId, uri);
1871
+ }
1872
+
1873
+ const globalGroupId = globalGroupIds.sort().join(',');
1874
+ const firstTag = tags[0];
1820
1875
  let codecs = '';
1821
1876
  /** @type {string} */
1822
- const type = this.getType_(tag);
1877
+ const type = this.getType_(firstTag);
1823
1878
  if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
1824
- codecs = tag.getAttributeValue('CODECS') || '';
1825
- } else if (groupId && this.groupIdToCodecsMap_.has(groupId)) {
1826
- codecs = this.groupIdToCodecsMap_.get(groupId);
1879
+ codecs = firstTag.getAttributeValue('CODECS') || '';
1880
+ } else {
1881
+ for (const groupId of globalGroupIds) {
1882
+ if (this.groupIdToCodecsMap_.has(groupId)) {
1883
+ codecs = this.groupIdToCodecsMap_.get(groupId);
1884
+ break;
1885
+ }
1886
+ }
1827
1887
  }
1828
1888
 
1829
- const verbatimMediaPlaylistUri = this.variableSubstitution_(
1830
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
1831
-
1832
1889
  // Check if the stream has already been created as part of another Variant
1833
1890
  // and return it if it has.
1834
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1835
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1891
+ const key = verbatimMediaPlaylistUris.sort().join(',');
1892
+ if (this.uriToStreamInfosMap_.has(key)) {
1893
+ return this.uriToStreamInfosMap_.get(key);
1894
+ }
1895
+ const streamId = this.globalId_++;
1896
+ if (this.contentSteeringManager_) {
1897
+ for (const [groupId, uri] of groupIdUriMappping) {
1898
+ const pathwayId = groupIdPathwayIdMapping.get(groupId);
1899
+ if (pathwayId) {
1900
+ this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
1901
+ }
1902
+ }
1836
1903
  }
1837
1904
 
1838
- const language = tag.getAttributeValue('LANGUAGE');
1839
- const name = tag.getAttributeValue('NAME');
1905
+ const language = firstTag.getAttributeValue('LANGUAGE');
1906
+ const name = firstTag.getAttributeValue('NAME');
1840
1907
 
1841
1908
  // NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
1842
1909
  // However, we don't bother to validate "AUTOSELECT", since we don't
@@ -1844,39 +1911,39 @@ shaka.hls.HlsParser = class {
1844
1911
  // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
1845
1912
  // only be selected explicitly by the user, and we don't have a way to
1846
1913
  // represent that in our model.
1847
- const defaultAttrValue = tag.getAttributeValue('DEFAULT');
1914
+ const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
1848
1915
  const primary = defaultAttrValue == 'YES';
1849
1916
 
1850
- const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
1851
- const spatialAudio = type == 'audio' ? this.isSpatialAudio_(tag) : false;
1852
- const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1917
+ const channelsCount =
1918
+ type == 'audio' ? this.getChannelsCount_(firstTag) : null;
1919
+ const spatialAudio =
1920
+ type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
1921
+ const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
1853
1922
 
1854
- const forcedAttrValue = tag.getAttributeValue('FORCED');
1923
+ const forcedAttrValue = firstTag.getAttributeValue('FORCED');
1855
1924
  const forced = forcedAttrValue == 'YES';
1856
- const sampleRate = type == 'audio' ? this.getSampleRate_(tag) : null;
1925
+ const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
1857
1926
  // TODO: Should we take into account some of the currently ignored
1858
1927
  // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
1859
1928
  const streamInfo = this.createStreamInfo_(
1860
- verbatimMediaPlaylistUri, codecs, type, language, primary, name,
1861
- channelsCount, /* closedCaptions= */ null, characteristics, forced,
1862
- sampleRate, spatialAudio);
1863
- if (this.groupIdToStreamInfosMap_.has(groupId)) {
1864
- this.groupIdToStreamInfosMap_.get(groupId).push(streamInfo);
1929
+ streamId, verbatimMediaPlaylistUris, codecs, type, language,
1930
+ primary, name, channelsCount, /* closedCaptions= */ null,
1931
+ characteristics, forced, sampleRate, spatialAudio);
1932
+ if (streamInfo.stream) {
1933
+ streamInfo.stream.groupId = globalGroupId;
1934
+ }
1935
+ if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
1936
+ this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
1865
1937
  } else {
1866
- this.groupIdToStreamInfosMap_.set(groupId, [streamInfo]);
1938
+ this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
1867
1939
  }
1868
1940
 
1869
- // TODO: This check is necessary because of the possibility of multiple
1870
- // calls to createStreamInfoFromMediaTag_ before either has resolved.
1871
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1872
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1873
- }
1874
- this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
1941
+ this.uriToStreamInfosMap_.set(key, streamInfo);
1875
1942
  return streamInfo;
1876
1943
  }
1877
1944
 
1878
1945
  /**
1879
- * Parse EXT-X-MEDIA media tag into a Stream object.
1946
+ * Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
1880
1947
  *
1881
1948
  * @param {shaka.hls.Tag} tag
1882
1949
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
@@ -1888,8 +1955,7 @@ shaka.hls.HlsParser = class {
1888
1955
  /** @type {string} */
1889
1956
  const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
1890
1957
 
1891
- const verbatimImagePlaylistUri = this.variableSubstitution_(
1892
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
1958
+ const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
1893
1959
  const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
1894
1960
 
1895
1961
  // Check if the stream has already been created as part of another Variant
@@ -1904,16 +1970,10 @@ shaka.hls.HlsParser = class {
1904
1970
  const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1905
1971
 
1906
1972
  const streamInfo = this.createStreamInfo_(
1907
- verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
1908
- name, /* channelsCount= */ null, /* closedCaptions= */ null,
1909
- characteristics, /* forced= */ false, /* sampleRate= */ null,
1910
- /* spatialAudio= */ false);
1911
-
1912
- // TODO: This check is necessary because of the possibility of multiple
1913
- // calls to createStreamInfoFromImageTag_ before either has resolved.
1914
- if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
1915
- return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
1916
- }
1973
+ this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
1974
+ /* primary= */ false, name, /* channelsCount= */ null,
1975
+ /* closedCaptions= */ null, characteristics, /* forced= */ false,
1976
+ /* sampleRate= */ null, /* spatialAudio= */ false);
1917
1977
 
1918
1978
  // Parse misc attributes.
1919
1979
  const resolution = tag.getAttributeValue('RESOLUTION');
@@ -1946,45 +2006,104 @@ shaka.hls.HlsParser = class {
1946
2006
  return streamInfo;
1947
2007
  }
1948
2008
 
2009
+ /**
2010
+ * Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
2011
+ *
2012
+ * @param {shaka.hls.Tag} tag
2013
+ * @return {!shaka.hls.HlsParser.StreamInfo}
2014
+ * @private
2015
+ */
2016
+ createStreamInfoFromIframeTag_(tag) {
2017
+ goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
2018
+ 'Should only be called on iframe tags!');
2019
+ /** @type {string} */
2020
+ const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
2021
+
2022
+ const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
2023
+ const codecs = tag.getAttributeValue('CODECS') || '';
2024
+
2025
+ // Check if the stream has already been created as part of another Variant
2026
+ // and return it if it has.
2027
+ if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
2028
+ return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
2029
+ }
2030
+
2031
+ const language = tag.getAttributeValue('LANGUAGE');
2032
+ const name = tag.getAttributeValue('NAME');
2033
+
2034
+ const characteristics = tag.getAttributeValue('CHARACTERISTICS');
2035
+
2036
+ const streamInfo = this.createStreamInfo_(
2037
+ this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
2038
+ /* primary= */ false, name, /* channelsCount= */ null,
2039
+ /* closedCaptions= */ null, characteristics, /* forced= */ false,
2040
+ /* sampleRate= */ null, /* spatialAudio= */ false);
2041
+
2042
+ // Parse misc attributes.
2043
+ const resolution = tag.getAttributeValue('RESOLUTION');
2044
+ const [width, height] = resolution ? resolution.split('x') : [null, null];
2045
+ streamInfo.stream.width = Number(width) || undefined;
2046
+ streamInfo.stream.height = Number(height) || undefined;
2047
+ const bandwidth = tag.getAttributeValue('BANDWIDTH');
2048
+ if (bandwidth) {
2049
+ streamInfo.stream.bandwidth = Number(bandwidth);
2050
+ }
2051
+
2052
+ this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
2053
+ return streamInfo;
2054
+ }
2055
+
1949
2056
  /**
1950
2057
  * Parse an EXT-X-STREAM-INF media tag into a Stream object.
1951
2058
  *
1952
- * @param {!shaka.hls.Tag} tag
2059
+ * @param {!Array.<!shaka.hls.Tag>} tags
1953
2060
  * @param {!Array.<string>} allCodecs
1954
2061
  * @param {string} type
2062
+ * @param {?string} language
2063
+ * @param {?string} name
2064
+ * @param {?number} channelsCount
2065
+ * @param {?string} characteristics
2066
+ * @param {?number} sampleRate
2067
+ * @param {boolean} spatialAudio
1955
2068
  * @return {!shaka.hls.HlsParser.StreamInfo}
1956
2069
  * @private
1957
2070
  */
1958
- createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1959
- goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
1960
- 'Should only be called on variant tags!');
1961
- const verbatimMediaPlaylistUri = this.variableSubstitution_(
1962
- tag.getRequiredAttrValue('URI'), this.globalVariables_);
2071
+ createStreamInfoFromVariantTags_(tags, allCodecs, type, language, name,
2072
+ channelsCount, characteristics, sampleRate, spatialAudio) {
2073
+ const streamId = this.globalId_++;
2074
+ const verbatimMediaPlaylistUris = [];
2075
+ for (const tag of tags) {
2076
+ goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
2077
+ 'Should only be called on variant tags!');
2078
+ const uri = tag.getRequiredAttrValue('URI');
2079
+ const pathwayId = tag.getAttributeValue('PATHWAY-ID');
2080
+ if (this.contentSteeringManager_ && pathwayId) {
2081
+ this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
2082
+ }
2083
+ verbatimMediaPlaylistUris.push(uri);
2084
+ }
1963
2085
 
1964
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1965
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
2086
+ const key = verbatimMediaPlaylistUris.sort().join(',');
2087
+ if (this.uriToStreamInfosMap_.has(key)) {
2088
+ return this.uriToStreamInfosMap_.get(key);
1966
2089
  }
1967
2090
 
1968
- const closedCaptions = this.getClosedCaptions_(tag, type);
2091
+ const closedCaptions = this.getClosedCaptions_(tags[0], type);
1969
2092
  const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
1970
- const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
1971
- codecs, type, /* language= */ null, /* primary= */ false,
1972
- /* name= */ null, /* channelcount= */ null, closedCaptions,
1973
- /* characteristics= */ null, /* forced= */ false,
1974
- /* sampleRate= */ null, /* spatialAudio= */ false);
1975
- // TODO: This check is necessary because of the possibility of multiple
1976
- // calls to createStreamInfoFromVariantTag_ before either has resolved.
1977
- if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
1978
- return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
1979
- }
2093
+ const streamInfo = this.createStreamInfo_(
2094
+ streamId, verbatimMediaPlaylistUris, codecs, type, language,
2095
+ /* primary= */ false, name, channelsCount, closedCaptions,
2096
+ characteristics, /* forced= */ false, sampleRate,
2097
+ /* spatialAudio= */ false);
1980
2098
 
1981
- this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
2099
+ this.uriToStreamInfosMap_.set(key, streamInfo);
1982
2100
  return streamInfo;
1983
2101
  }
1984
2102
 
1985
2103
 
1986
2104
  /**
1987
- * @param {string} verbatimMediaPlaylistUri
2105
+ * @param {number} streamId
2106
+ * @param {!Array.<string>} verbatimMediaPlaylistUris
1988
2107
  * @param {string} codecs
1989
2108
  * @param {string} type
1990
2109
  * @param {?string} languageValue
@@ -1999,25 +2118,34 @@ shaka.hls.HlsParser = class {
1999
2118
  * @return {!shaka.hls.HlsParser.StreamInfo}
2000
2119
  * @private
2001
2120
  */
2002
- createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, languageValue,
2003
- primary, name, channelsCount, closedCaptions, characteristics, forced,
2004
- sampleRate, spatialAudio) {
2121
+ createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
2122
+ languageValue, primary, name, channelsCount, closedCaptions,
2123
+ characteristics, forced, sampleRate, spatialAudio) {
2005
2124
  // TODO: Refactor, too many parameters
2006
- const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
2007
- this.masterPlaylistUri_, verbatimMediaPlaylistUri);
2008
2125
 
2009
2126
  // This stream is lazy-loaded inside the createSegmentIndex function.
2010
2127
  // So we start out with a stream object that does not contain the actual
2011
2128
  // segment index, then download when createSegmentIndex is called.
2012
- const stream = this.makeStreamObject_(codecs, type, languageValue, primary,
2013
- name, channelsCount, closedCaptions, characteristics, forced,
2014
- sampleRate, spatialAudio);
2129
+ const stream = this.makeStreamObject_(streamId, codecs, type,
2130
+ languageValue, primary, name, channelsCount, closedCaptions,
2131
+ characteristics, forced, sampleRate, spatialAudio);
2132
+
2133
+ const redirectUris = [];
2134
+ const getUris = () => {
2135
+ if (this.contentSteeringManager_ &&
2136
+ verbatimMediaPlaylistUris.length > 1) {
2137
+ return this.contentSteeringManager_.getLocations(streamId);
2138
+ }
2139
+ return redirectUris.concat(shaka.hls.Utils.constructUris(
2140
+ [this.masterPlaylistUri_], verbatimMediaPlaylistUris,
2141
+ this.globalVariables_));
2142
+ };
2015
2143
  const streamInfo = {
2016
2144
  stream,
2017
2145
  type,
2018
- verbatimMediaPlaylistUri,
2146
+ redirectUris,
2147
+ getUris,
2019
2148
  // These values are filled out or updated after lazy-loading:
2020
- absoluteMediaPlaylistUri: initialMediaPlaylistUri,
2021
2149
  minTimestamp: 0,
2022
2150
  maxTimestamp: 0,
2023
2151
  mediaSequenceToStartTime: new Map(),
@@ -2032,27 +2160,45 @@ shaka.hls.HlsParser = class {
2032
2160
 
2033
2161
  /** @param {!AbortSignal} abortSignal */
2034
2162
  const downloadSegmentIndex = async (abortSignal) => {
2163
+ const uris = streamInfo.getUris();
2035
2164
  // Download the actual manifest.
2036
2165
  const response = await this.requestManifest_(
2037
- streamInfo.absoluteMediaPlaylistUri, /* isPlaylist= */ true);
2166
+ streamInfo.getUris(), /* isPlaylist= */ true);
2038
2167
  if (abortSignal.aborted) {
2039
2168
  return;
2040
2169
  }
2041
2170
 
2042
2171
  // Record the final URI after redirects.
2043
- const absoluteMediaPlaylistUri = response.uri;
2172
+ const responseUri = response.uri;
2173
+ if (responseUri != response.originalUri && !uris.includes(responseUri)) {
2174
+ redirectUris.push(responseUri);
2175
+ }
2044
2176
 
2045
2177
  // Record the redirected, final URI of this media playlist when we parse
2046
2178
  // it.
2047
2179
  /** @type {!shaka.hls.Playlist} */
2048
- const playlist = this.manifestTextParser_.parsePlaylist(
2049
- response.data, absoluteMediaPlaylistUri);
2180
+ const playlist = this.manifestTextParser_.parsePlaylist(response.data);
2181
+
2182
+ let mimeType = undefined;
2183
+
2184
+ // If no codec info was provided in the manifest and codec guessing is
2185
+ // disabled we try to get necessary info from the media data.
2186
+ if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
2187
+ const basicInfo =
2188
+ await this.getMediaPlaylistBasicInfo_(playlist, getUris);
2189
+
2190
+ goog.asserts.assert(
2191
+ type === basicInfo.type, 'Media types should match!');
2192
+
2193
+ mimeType = basicInfo.mimeType;
2194
+ codecs = basicInfo.codecs;
2195
+ }
2050
2196
 
2051
2197
  const wasLive = this.isLive_();
2052
2198
  const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
2053
- playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
2199
+ streamId, playlist, getUris, responseUri, codecs,
2054
2200
  type, languageValue, primary, name, channelsCount, closedCaptions,
2055
- characteristics, forced, sampleRate, spatialAudio);
2201
+ characteristics, forced, sampleRate, spatialAudio, mimeType);
2056
2202
  if (abortSignal.aborted) {
2057
2203
  return;
2058
2204
  }
@@ -2066,7 +2212,6 @@ shaka.hls.HlsParser = class {
2066
2212
  }
2067
2213
 
2068
2214
  // Copy values from the real stream info to our initial one.
2069
- streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
2070
2215
  streamInfo.minTimestamp = realStreamInfo.minTimestamp;
2071
2216
  streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
2072
2217
  streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
@@ -2083,6 +2228,7 @@ shaka.hls.HlsParser = class {
2083
2228
  stream.keyIds = realStream.keyIds;
2084
2229
  stream.mimeType = realStream.mimeType;
2085
2230
  stream.bandwidth = realStream.bandwidth;
2231
+ stream.codecs = realStream.codecs || stream.codecs;
2086
2232
 
2087
2233
  // Since we lazy-loaded this content, the player may need to create new
2088
2234
  // sessions for the DRM info in this stream.
@@ -2284,9 +2430,10 @@ shaka.hls.HlsParser = class {
2284
2430
  }
2285
2431
 
2286
2432
  /**
2433
+ * @param {number} streamId
2287
2434
  * @param {!shaka.hls.Playlist} playlist
2288
- * @param {string} verbatimMediaPlaylistUri
2289
- * @param {string} absoluteMediaPlaylistUri
2435
+ * @param {function():!Array.<string>} getUris
2436
+ * @param {string} responseUri
2290
2437
  * @param {string} codecs
2291
2438
  * @param {string} type
2292
2439
  * @param {?string} languageValue
@@ -2302,8 +2449,8 @@ shaka.hls.HlsParser = class {
2302
2449
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
2303
2450
  * @private
2304
2451
  */
2305
- async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri,
2306
- absoluteMediaPlaylistUri, codecs, type, languageValue, primary, name,
2452
+ async convertParsedPlaylistIntoStreamInfo_(streamId, playlist,
2453
+ getUris, responseUri, codecs, type, languageValue, primary, name,
2307
2454
  channelsCount, closedCaptions, characteristics, forced, sampleRate,
2308
2455
  spatialAudio, mimeType = undefined) {
2309
2456
  if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
@@ -2319,7 +2466,8 @@ shaka.hls.HlsParser = class {
2319
2466
  const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
2320
2467
  'EXT-X-DEFINE');
2321
2468
 
2322
- const mediaVariables = this.parseMediaVariables_(variablesTags);
2469
+ const mediaVariables =
2470
+ this.parseMediaVariables_(variablesTags, responseUri);
2323
2471
 
2324
2472
  goog.asserts.assert(playlist.segments != null,
2325
2473
  'Media playlist should have segments!');
@@ -2332,7 +2480,7 @@ shaka.hls.HlsParser = class {
2332
2480
 
2333
2481
  if (!mimeType) {
2334
2482
  mimeType = await this.guessMimeType_(type, codecs, playlist,
2335
- mediaVariables);
2483
+ mediaVariables, getUris);
2336
2484
  }
2337
2485
 
2338
2486
  const {drmInfos, keyIds, encrypted, aesEncrypted} =
@@ -2345,11 +2493,20 @@ shaka.hls.HlsParser = class {
2345
2493
  shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
2346
2494
  }
2347
2495
 
2496
+ const stream = this.makeStreamObject_(streamId, codecs, type,
2497
+ languageValue, primary, name, channelsCount, closedCaptions,
2498
+ characteristics, forced, sampleRate, spatialAudio);
2499
+ stream.encrypted = encrypted;
2500
+ stream.drmInfos = drmInfos;
2501
+ stream.keyIds = keyIds;
2502
+ stream.mimeType = mimeType;
2503
+
2348
2504
  const mediaSequenceToStartTime = this.isLive_() ?
2349
2505
  this.mediaSequenceToStartTimeByType_.get(type) : new Map();
2506
+
2350
2507
  const {segments, bandwidth} = this.createSegments_(
2351
- verbatimMediaPlaylistUri, playlist, type, mimeType,
2352
- mediaSequenceToStartTime, mediaVariables);
2508
+ playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
2509
+ stream.bandwidth = bandwidth;
2353
2510
 
2354
2511
  // This new calculation is necessary for Low Latency streams.
2355
2512
  if (this.isLive_()) {
@@ -2361,6 +2518,7 @@ shaka.hls.HlsParser = class {
2361
2518
  const lastEndTime = lastSegment.endTime;
2362
2519
  /** @type {!shaka.media.SegmentIndex} */
2363
2520
  const segmentIndex = new shaka.media.SegmentIndex(segments);
2521
+ stream.segmentIndex = segmentIndex;
2364
2522
 
2365
2523
  const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
2366
2524
  playlist.tags, 'EXT-X-SERVER-CONTROL');
@@ -2375,21 +2533,11 @@ shaka.hls.HlsParser = class {
2375
2533
  const {nextMediaSequence, nextPart} =
2376
2534
  this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
2377
2535
 
2378
- const stream = this.makeStreamObject_(codecs, type, languageValue, primary,
2379
- name, channelsCount, closedCaptions, characteristics, forced,
2380
- sampleRate, spatialAudio);
2381
- stream.segmentIndex = segmentIndex;
2382
- stream.encrypted = encrypted;
2383
- stream.drmInfos = drmInfos;
2384
- stream.keyIds = keyIds;
2385
- stream.mimeType = mimeType;
2386
- stream.bandwidth = bandwidth;
2387
-
2388
2536
  return {
2389
2537
  stream,
2390
2538
  type,
2391
- verbatimMediaPlaylistUri,
2392
- absoluteMediaPlaylistUri,
2539
+ redirectUris: [],
2540
+ getUris,
2393
2541
  minTimestamp: firstStartTime,
2394
2542
  maxTimestamp: lastEndTime,
2395
2543
  canSkipSegments,
@@ -2453,6 +2601,7 @@ shaka.hls.HlsParser = class {
2453
2601
  * The parameters that are passed into here are only the things that can be
2454
2602
  * known without downloading the media playlist; other values must be set
2455
2603
  * manually on the object after creation.
2604
+ * @param {number} id
2456
2605
  * @param {string} codecs
2457
2606
  * @param {string} type
2458
2607
  * @param {?string} languageValue
@@ -2467,8 +2616,9 @@ shaka.hls.HlsParser = class {
2467
2616
  * @return {!shaka.extern.Stream}
2468
2617
  * @private
2469
2618
  */
2470
- makeStreamObject_(codecs, type, languageValue, primary, name, channelsCount,
2471
- closedCaptions, characteristics, forced, sampleRate, spatialAudio) {
2619
+ makeStreamObject_(id, codecs, type, languageValue, primary, name,
2620
+ channelsCount, closedCaptions, characteristics, forced, sampleRate,
2621
+ spatialAudio) {
2472
2622
  // Fill out a "best-guess" mimeType, for now. It will be replaced once the
2473
2623
  // stream is lazy-loaded.
2474
2624
  const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
@@ -2480,8 +2630,21 @@ shaka.hls.HlsParser = class {
2480
2630
  }
2481
2631
  }
2482
2632
 
2483
- const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2484
- shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
2633
+ let kind = undefined;
2634
+ let accessibilityPurpose = null;
2635
+ if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2636
+ if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
2637
+ roles.includes('public.accessibility.describes-music-and-sound')) {
2638
+ kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
2639
+ } else {
2640
+ kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
2641
+ }
2642
+ } else {
2643
+ if (roles.includes('public.accessibility.describes-video')) {
2644
+ accessibilityPurpose =
2645
+ shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
2646
+ }
2647
+ }
2485
2648
 
2486
2649
  // If there are no roles, and we have defaulted to the subtitle "kind" for
2487
2650
  // this track, add the implied subtitle role.
@@ -2493,6 +2656,7 @@ shaka.hls.HlsParser = class {
2493
2656
  return {
2494
2657
  id: this.globalId_++,
2495
2658
  originalId: name,
2659
+ groupId: null,
2496
2660
  createSegmentIndex: () => Promise.resolve(),
2497
2661
  segmentIndex: null,
2498
2662
  mimeType,
@@ -2522,9 +2686,11 @@ shaka.hls.HlsParser = class {
2522
2686
  spatialAudio,
2523
2687
  closedCaptions,
2524
2688
  hdr: undefined,
2689
+ videoLayout: undefined,
2525
2690
  tilesLayout: undefined,
2526
- accessibilityPurpose: null,
2691
+ accessibilityPurpose: accessibilityPurpose,
2527
2692
  external: false,
2693
+ fastSwitching: false,
2528
2694
  };
2529
2695
  }
2530
2696
 
@@ -2595,10 +2761,12 @@ shaka.hls.HlsParser = class {
2595
2761
  /**
2596
2762
  * @param {!shaka.hls.Tag} drmTag
2597
2763
  * @param {!shaka.hls.Playlist} playlist
2598
- * @return {!shaka.extern.HlsAes128Key}
2764
+ * @param {function():!Array.<string>} getUris
2765
+ * @param {?Map.<string, string>=} variables
2766
+ * @return {!shaka.extern.aes128Key}
2599
2767
  * @private
2600
2768
  */
2601
- parseAES128DrmTag_(drmTag, playlist) {
2769
+ parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
2602
2770
  // Check if the Web Crypto API is available.
2603
2771
  if (!window.crypto || !window.crypto.subtle) {
2604
2772
  shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
@@ -2629,22 +2797,21 @@ shaka.hls.HlsParser = class {
2629
2797
  throw new shaka.util.Error(
2630
2798
  shaka.util.Error.Severity.CRITICAL,
2631
2799
  shaka.util.Error.Category.MANIFEST,
2632
- shaka.util.Error.Code.HLS_AES_128_INVALID_IV_LENGTH);
2800
+ shaka.util.Error.Code.AES_128_INVALID_IV_LENGTH);
2633
2801
  }
2634
2802
  }
2635
2803
 
2636
- const keyUri = shaka.hls.Utils.constructAbsoluteUri(
2637
- playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
2638
-
2639
- const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2640
- const request = shaka.net.NetworkingEngine.makeRequest(
2641
- [keyUri], this.config_.retryParameters);
2642
-
2643
2804
  const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
2644
2805
 
2645
2806
  // Don't download the key object until the segment is parsed, to avoid a
2646
2807
  // startup delay for long manifests with lots of keys.
2647
2808
  keyInfo.fetchKey = async () => {
2809
+ const keyUri = shaka.hls.Utils.constructSegmentUris(
2810
+ getUris(), drmTag.getRequiredAttrValue('URI'), variables);
2811
+
2812
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2813
+ const request = shaka.net.NetworkingEngine.makeRequest(
2814
+ [keyUri], this.config_.retryParameters);
2648
2815
  const keyResponse = await this.makeNetworkRequest_(request, requestType);
2649
2816
 
2650
2817
  // keyResponse.status is undefined when URI is "data:text/plain;base64,"
@@ -2652,7 +2819,7 @@ shaka.hls.HlsParser = class {
2652
2819
  throw new shaka.util.Error(
2653
2820
  shaka.util.Error.Severity.CRITICAL,
2654
2821
  shaka.util.Error.Category.MANIFEST,
2655
- shaka.util.Error.Code.HLS_AES_128_INVALID_KEY_LENGTH);
2822
+ shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
2656
2823
  }
2657
2824
 
2658
2825
  const algorithm = {
@@ -2821,11 +2988,12 @@ shaka.hls.HlsParser = class {
2821
2988
  * Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
2822
2989
  * @param {!shaka.hls.Playlist} playlist
2823
2990
  * @param {!Array.<!shaka.hls.Tag>} tags Segment tags
2824
- * @param {!Map.<string, string>} variables
2991
+ * @param {function():!Array.<string>} getUris
2992
+ * @param {?Map.<string, string>=} variables
2825
2993
  * @return {shaka.media.InitSegmentReference}
2826
2994
  * @private
2827
2995
  */
2828
- getInitSegmentReference_(playlist, tags, variables) {
2996
+ getInitSegmentReference_(playlist, tags, getUris, variables) {
2829
2997
  /** @type {?shaka.hls.Tag} */
2830
2998
  const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
2831
2999
 
@@ -2834,28 +3002,30 @@ shaka.hls.HlsParser = class {
2834
3002
  }
2835
3003
  // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
2836
3004
  const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
2837
- const absoluteInitSegmentUri = this.variableSubstitution_(
2838
- shaka.hls.Utils.constructAbsoluteUri(
2839
- playlist.absoluteUri, verbatimInitSegmentUri),
2840
- variables);
3005
+ const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
3006
+ getUris(), verbatimInitSegmentUri, variables);
2841
3007
 
2842
3008
  const mapTagKey = [
2843
- absoluteInitSegmentUri,
3009
+ absoluteInitSegmentUris.toString(),
2844
3010
  mapTag.getAttributeValue('BYTERANGE', ''),
2845
3011
  ].join('-');
2846
3012
  if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
2847
- /** @type {shaka.extern.HlsAes128Key|undefined} */
3013
+ /** @type {shaka.extern.aes128Key|undefined} */
2848
3014
  let aes128Key = undefined;
3015
+ let byteRangeTag = null;
2849
3016
  for (const tag of tags) {
2850
3017
  if (tag.name == 'EXT-X-KEY') {
2851
3018
  if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
2852
3019
  tag.id < mapTag.id) {
2853
- aes128Key = this.parseAES128DrmTag_(tag, playlist);
3020
+ aes128Key =
3021
+ this.parseAES128DrmTag_(tag, playlist, getUris, variables);
2854
3022
  }
3023
+ } else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
3024
+ byteRangeTag = tag;
2855
3025
  }
2856
3026
  }
2857
3027
  const initSegmentRef = this.createInitSegmentReference_(
2858
- absoluteInitSegmentUri, mapTag, aes128Key);
3028
+ absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
2859
3029
  this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
2860
3030
  }
2861
3031
  return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
@@ -2864,16 +3034,21 @@ shaka.hls.HlsParser = class {
2864
3034
  /**
2865
3035
  * Create an InitSegmentReference object for the EXT-X-MAP tag in the media
2866
3036
  * playlist.
2867
- * @param {string} absoluteInitSegmentUri
3037
+ * @param {!Array.<string>} absoluteInitSegmentUris
2868
3038
  * @param {!shaka.hls.Tag} mapTag EXT-X-MAP
2869
- * @param {shaka.extern.HlsAes128Key=} aes128Key
3039
+ * @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
3040
+ * @param {shaka.extern.aes128Key=} aes128Key
2870
3041
  * @return {!shaka.media.InitSegmentReference}
2871
3042
  * @private
2872
3043
  */
2873
- createInitSegmentReference_(absoluteInitSegmentUri, mapTag, aes128Key) {
3044
+ createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
3045
+ aes128Key) {
2874
3046
  let startByte = 0;
2875
3047
  let endByte = null;
2876
- const byterange = mapTag.getAttributeValue('BYTERANGE');
3048
+ let byterange = mapTag.getAttributeValue('BYTERANGE');
3049
+ if (!byterange && byteRangeTag) {
3050
+ byterange = byteRangeTag.value;
3051
+ }
2877
3052
  // If a BYTERANGE attribute is not specified, the segment consists
2878
3053
  // of the entire resource.
2879
3054
  if (byterange) {
@@ -2894,7 +3069,7 @@ shaka.hls.HlsParser = class {
2894
3069
  }
2895
3070
 
2896
3071
  const initSegmentRef = new shaka.media.InitSegmentReference(
2897
- () => [absoluteInitSegmentUri],
3072
+ () => absoluteInitSegmentUris,
2898
3073
  startByte,
2899
3074
  endByte,
2900
3075
  /* mediaQuality= */ null,
@@ -2912,16 +3087,16 @@ shaka.hls.HlsParser = class {
2912
3087
  * @param {!shaka.hls.Segment} hlsSegment
2913
3088
  * @param {number} startTime
2914
3089
  * @param {!Map.<string, string>} variables
2915
- * @param {string} absoluteMediaPlaylistUri
2916
- * @param {string} type
2917
- * @param {string} mimeType
2918
- * @param {shaka.extern.HlsAes128Key=} hlsAes128Key
3090
+ * @param {!shaka.hls.Playlist} playlist
3091
+ * @param {shaka.extern.Stream} stream
3092
+ * @param {function():!Array.<string>} getUris
3093
+ * @param {shaka.extern.aes128Key=} aes128Key
2919
3094
  * @return {shaka.media.SegmentReference}
2920
3095
  * @private
2921
3096
  */
2922
3097
  createSegmentReference_(
2923
3098
  initSegmentReference, previousReference, hlsSegment, startTime,
2924
- variables, absoluteMediaPlaylistUri, type, mimeType, hlsAes128Key) {
3099
+ variables, playlist, stream, getUris, aes128Key) {
2925
3100
  const tags = hlsSegment.tags;
2926
3101
  const extinfTag =
2927
3102
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
@@ -2978,8 +3153,9 @@ shaka.hls.HlsParser = class {
2978
3153
  let isPreloadSegment = false;
2979
3154
 
2980
3155
  if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
2981
- const byterangeOptimizationSupport = (mimeType == 'video/mp4' ||
2982
- mimeType == 'audio/mp4') && window.ReadableStream;
3156
+ const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' ||
3157
+ stream.mimeType == 'audio/mp4') && window.ReadableStream &&
3158
+ this.config_.hls.allowLowLatencyByteRangeOptimization;
2983
3159
 
2984
3160
  let partialSyncTime = syncTime;
2985
3161
  for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
@@ -3035,14 +3211,14 @@ shaka.hls.HlsParser = class {
3035
3211
  somePartialSegmentWithGap = true;
3036
3212
  }
3037
3213
 
3038
- let pAbsoluteUri = null;
3214
+ let uris = null;
3039
3215
  const getPartialUris = () => {
3040
- if (pAbsoluteUri == null) {
3216
+ if (uris == null) {
3041
3217
  goog.asserts.assert(pUri, 'Partial uri should be defined!');
3042
- pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri(
3043
- absoluteMediaPlaylistUri, pUri);
3218
+ uris = shaka.hls.Utils.constructSegmentUris(
3219
+ getUris(), pUri, variables);
3044
3220
  }
3045
- return [pAbsoluteUri];
3221
+ return uris;
3046
3222
  };
3047
3223
 
3048
3224
  if (byterangeOptimizationSupport &&
@@ -3066,7 +3242,7 @@ shaka.hls.HlsParser = class {
3066
3242
  /* tileDuration= */ null,
3067
3243
  partialSyncTime,
3068
3244
  partialStatus,
3069
- hlsAes128Key);
3245
+ aes128Key);
3070
3246
  if (item.name == 'EXT-X-PRELOAD-HINT') {
3071
3247
  partial.markAsPreload();
3072
3248
  isPreloadSegment = true;
@@ -3137,7 +3313,7 @@ shaka.hls.HlsParser = class {
3137
3313
 
3138
3314
  let tilesLayout = '';
3139
3315
  let tileDuration = null;
3140
- if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
3316
+ if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
3141
3317
  // By default in HLS the tilesLayout is 1x1
3142
3318
  tilesLayout = '1x1';
3143
3319
  const tilesTag =
@@ -3151,22 +3327,25 @@ shaka.hls.HlsParser = class {
3151
3327
  }
3152
3328
  }
3153
3329
 
3154
- let absoluteSegmentUri = null;
3155
- const getUris = () => {
3330
+ let uris = null;
3331
+ const getSegmentUris = () => {
3156
3332
  if (getUrisOptimization) {
3157
3333
  return getUrisOptimization();
3158
3334
  }
3159
- if (absoluteSegmentUri == null) {
3160
- absoluteSegmentUri = this.variableSubstitution_(
3161
- hlsSegment.absoluteUri, variables);
3335
+ if (uris == null) {
3336
+ uris = shaka.hls.Utils.constructSegmentUris(getUris(),
3337
+ hlsSegment.verbatimSegmentUri, variables);
3162
3338
  }
3163
- return absoluteSegmentUri.length ? [absoluteSegmentUri] : [];
3339
+ return uris || [];
3164
3340
  };
3165
3341
 
3342
+ const allPartialSegments = partialSegmentRefs.length > 0 &&
3343
+ !!hlsSegment.verbatimSegmentUri;
3344
+
3166
3345
  const reference = new shaka.media.SegmentReference(
3167
3346
  startTime,
3168
3347
  endTime,
3169
- getUris,
3348
+ getSegmentUris,
3170
3349
  startByte,
3171
3350
  endByte,
3172
3351
  initSegmentReference,
@@ -3178,7 +3357,8 @@ shaka.hls.HlsParser = class {
3178
3357
  tileDuration,
3179
3358
  syncTime,
3180
3359
  status,
3181
- hlsAes128Key,
3360
+ aes128Key,
3361
+ allPartialSegments,
3182
3362
  );
3183
3363
 
3184
3364
  if (segmentWithByteRangeOptimization) {
@@ -3226,18 +3406,17 @@ shaka.hls.HlsParser = class {
3226
3406
  * get the bandwidth necessary for this segments If it's defined in the
3227
3407
  * playlist.
3228
3408
  *
3229
- * @param {string} verbatimMediaPlaylistUri
3230
3409
  * @param {!shaka.hls.Playlist} playlist
3231
- * @param {string} type
3232
- * @param {string} mimeType
3410
+ * @param {shaka.extern.Stream} stream
3233
3411
  * @param {!Map.<number, number>} mediaSequenceToStartTime
3234
3412
  * @param {!Map.<string, string>} variables
3413
+ * @param {function():!Array.<string>} getUris
3235
3414
  * @return {{segments: !Array.<!shaka.media.SegmentReference>,
3236
3415
  * bandwidth: (number|undefined)}}
3237
3416
  * @private
3238
3417
  */
3239
- createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
3240
- mediaSequenceToStartTime, variables) {
3418
+ createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
3419
+ getUris) {
3241
3420
  /** @type {Array.<!shaka.hls.Segment>} */
3242
3421
  const hlsSegments = playlist.segments;
3243
3422
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -3245,8 +3424,8 @@ shaka.hls.HlsParser = class {
3245
3424
  /** @type {shaka.media.InitSegmentReference} */
3246
3425
  let initSegmentRef;
3247
3426
 
3248
- /** @type {shaka.extern.HlsAes128Key|undefined} */
3249
- let hlsAes128Key = undefined;
3427
+ /** @type {shaka.extern.aes128Key|undefined} */
3428
+ let aes128Key = undefined;
3250
3429
 
3251
3430
  let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
3252
3431
  playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
@@ -3289,9 +3468,10 @@ shaka.hls.HlsParser = class {
3289
3468
  for (const drmTag of item.tags) {
3290
3469
  if (drmTag.name == 'EXT-X-KEY') {
3291
3470
  if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
3292
- hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
3471
+ aes128Key =
3472
+ this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
3293
3473
  } else {
3294
- hlsAes128Key = undefined;
3474
+ aes128Key = undefined;
3295
3475
  }
3296
3476
  }
3297
3477
  }
@@ -3299,7 +3479,7 @@ shaka.hls.HlsParser = class {
3299
3479
  mediaSequenceToStartTime.set(position, startTime);
3300
3480
 
3301
3481
  initSegmentRef = this.getInitSegmentReference_(playlist,
3302
- item.tags, variables);
3482
+ item.tags, getUris, variables);
3303
3483
 
3304
3484
  // If the stream is low latency and the user has not configured the
3305
3485
  // lowLatencyMode, but if it has been configured to activate the
@@ -3319,10 +3499,10 @@ shaka.hls.HlsParser = class {
3319
3499
  item,
3320
3500
  startTime,
3321
3501
  variables,
3322
- playlist.absoluteUri,
3323
- type,
3324
- mimeType,
3325
- hlsAes128Key);
3502
+ playlist,
3503
+ stream,
3504
+ getUris,
3505
+ aes128Key);
3326
3506
 
3327
3507
  if (reference) {
3328
3508
  const bitrate = shaka.hls.Utils.getFirstTagWithNameAsNumber(
@@ -3455,42 +3635,6 @@ shaka.hls.HlsParser = class {
3455
3635
  };
3456
3636
  }
3457
3637
 
3458
- /**
3459
- * Replaces the variables of a given URI.
3460
- *
3461
- * @param {string} uri
3462
- * @param {!Map.<string, string>} variables
3463
- * @return {string}
3464
- * @private
3465
- */
3466
- variableSubstitution_(uri, variables) {
3467
- if (!variables.size) {
3468
- return uri;
3469
- }
3470
- let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
3471
-
3472
- const uriVariables = newUri.match(/{\$\w*}/g);
3473
- if (uriVariables) {
3474
- for (const variable of uriVariables) {
3475
- // Note: All variables have the structure {$...}
3476
- const variableName = variable.slice(2, variable.length - 1);
3477
- const replaceValue = variables.get(variableName);
3478
- if (replaceValue) {
3479
- newUri = newUri.replace(variable, replaceValue);
3480
- } else {
3481
- shaka.log.error('A variable has been found that is not declared',
3482
- variableName);
3483
- throw new shaka.util.Error(
3484
- shaka.util.Error.Severity.CRITICAL,
3485
- shaka.util.Error.Category.MANIFEST,
3486
- shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND,
3487
- variableName);
3488
- }
3489
- }
3490
- }
3491
- return newUri;
3492
- }
3493
-
3494
3638
  /**
3495
3639
  * Attempts to guess stream's mime type based on content type and URI.
3496
3640
  *
@@ -3524,6 +3668,10 @@ shaka.hls.HlsParser = class {
3524
3668
  }
3525
3669
  }
3526
3670
 
3671
+ if (codecs == 'mjpg') {
3672
+ return 'application/mp4';
3673
+ }
3674
+
3527
3675
  // Not enough information to guess from the content type and codecs.
3528
3676
  return null;
3529
3677
  }
@@ -3556,10 +3704,11 @@ shaka.hls.HlsParser = class {
3556
3704
  * @param {string} codecs
3557
3705
  * @param {!shaka.hls.Playlist} playlist
3558
3706
  * @param {!Map.<string, string>} variables
3707
+ * @param {function():!Array.<string>} getUris
3559
3708
  * @return {!Promise.<string>}
3560
3709
  * @private
3561
3710
  */
3562
- async guessMimeType_(contentType, codecs, playlist, variables) {
3711
+ async guessMimeType_(contentType, codecs, playlist, variables, getUris) {
3563
3712
  const HlsParser = shaka.hls.HlsParser;
3564
3713
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
3565
3714
 
@@ -3569,10 +3718,13 @@ shaka.hls.HlsParser = class {
3569
3718
  goog.asserts.assert(playlist.segments.length,
3570
3719
  'Playlist should have segments!');
3571
3720
  const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
3572
- const middleSegmentUri = this.variableSubstitution_(
3573
- playlist.segments[middleSegmentIdx].absoluteUri, variables);
3574
3721
 
3575
- const parsedUri = new goog.Uri(middleSegmentUri);
3722
+ const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
3723
+ getUris(),
3724
+ playlist.segments[middleSegmentIdx].verbatimSegmentUri,
3725
+ variables);
3726
+
3727
+ const parsedUri = new goog.Uri(middleSegmentUris[0]);
3576
3728
  const extension = parsedUri.getPath().split('.').pop();
3577
3729
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
3578
3730
 
@@ -3595,7 +3747,7 @@ shaka.hls.HlsParser = class {
3595
3747
  // If unable to guess mime type, request a segment and try getting it
3596
3748
  // from the response.
3597
3749
  const headRequest = shaka.net.NetworkingEngine.makeRequest(
3598
- [middleSegmentUri], this.config_.retryParameters);
3750
+ middleSegmentUris, this.config_.retryParameters);
3599
3751
  headRequest.method = 'HEAD';
3600
3752
  const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
3601
3753
  const response = await this.makeNetworkRequest_(
@@ -3637,14 +3789,17 @@ shaka.hls.HlsParser = class {
3637
3789
  * @param {?string} height
3638
3790
  * @param {?string} frameRate
3639
3791
  * @param {?string} videoRange
3792
+ * @param {?string} videoLayout
3640
3793
  * @private
3641
3794
  */
3642
- addVideoAttributes_(stream, width, height, frameRate, videoRange) {
3795
+ addVideoAttributes_(stream, width, height, frameRate, videoRange,
3796
+ videoLayout) {
3643
3797
  if (stream) {
3644
3798
  stream.width = Number(width) || undefined;
3645
3799
  stream.height = Number(height) || undefined;
3646
3800
  stream.frameRate = Number(frameRate) || undefined;
3647
3801
  stream.hdr = videoRange || undefined;
3802
+ stream.videoLayout = videoLayout || undefined;
3648
3803
  }
3649
3804
  }
3650
3805
 
@@ -3652,16 +3807,16 @@ shaka.hls.HlsParser = class {
3652
3807
  * Makes a network request for the manifest and returns a Promise
3653
3808
  * with the resulting data.
3654
3809
  *
3655
- * @param {string} absoluteUri
3810
+ * @param {!Array.<string>} uris
3656
3811
  * @param {boolean=} isPlaylist
3657
3812
  * @return {!Promise.<!shaka.extern.Response>}
3658
3813
  * @private
3659
3814
  */
3660
- requestManifest_(absoluteUri, isPlaylist) {
3815
+ requestManifest_(uris, isPlaylist) {
3661
3816
  const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
3662
3817
 
3663
3818
  const request = shaka.net.NetworkingEngine.makeRequest(
3664
- [absoluteUri], this.config_.retryParameters);
3819
+ uris, this.config_.retryParameters);
3665
3820
 
3666
3821
  const type = isPlaylist ?
3667
3822
  shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
@@ -3690,12 +3845,20 @@ shaka.hls.HlsParser = class {
3690
3845
  }
3691
3846
 
3692
3847
  try {
3848
+ const startTime = Date.now();
3693
3849
  await this.update();
3694
3850
 
3851
+ // Keep track of how long the longest manifest update took.
3852
+ const endTime = Date.now();
3853
+
3695
3854
  // This may have converted to VOD, in which case we stop updating.
3696
3855
  if (this.isLive_()) {
3856
+ const updateDuration = (endTime - startTime) / 1000.0;
3857
+ this.averageUpdateDuration_.sample(1, updateDuration);
3697
3858
  const delay = this.getUpdatePlaylistDelay_();
3698
- this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
3859
+ const finalDelay = Math.max(0,
3860
+ delay - this.averageUpdateDuration_.getEstimate());
3861
+ this.updatePlaylistTimer_.tickAfter(/* seconds= */ finalDelay);
3699
3862
  }
3700
3863
  } catch (error) {
3701
3864
  // Detect a call to stop() during this.update()
@@ -3718,6 +3881,13 @@ shaka.hls.HlsParser = class {
3718
3881
  // Try again very soon.
3719
3882
  this.updatePlaylistTimer_.tickAfter(/* seconds= */ 0.1);
3720
3883
  }
3884
+
3885
+ // Detect a call to stop()
3886
+ if (!this.playerInterface_) {
3887
+ return;
3888
+ }
3889
+
3890
+ this.playerInterface_.onManifestUpdated();
3721
3891
  }
3722
3892
 
3723
3893
 
@@ -3932,8 +4102,8 @@ shaka.hls.HlsParser = class {
3932
4102
  * @typedef {{
3933
4103
  * stream: !shaka.extern.Stream,
3934
4104
  * type: string,
3935
- * verbatimMediaPlaylistUri: string,
3936
- * absoluteMediaPlaylistUri: string,
4105
+ * redirectUris: !Array.<string>,
4106
+ * getUris: function():!Array.<string>,
3937
4107
  * minTimestamp: number,
3938
4108
  * maxTimestamp: number,
3939
4109
  * mediaSequenceToStartTime: !Map.<number, number>,
@@ -3953,14 +4123,10 @@ shaka.hls.HlsParser = class {
3953
4123
  * The Stream itself.
3954
4124
  * @property {string} type
3955
4125
  * The type value. Could be 'video', 'audio', 'text', or 'image'.
3956
- * @property {string} verbatimMediaPlaylistUri
3957
- * The verbatim media playlist URI, as it appeared in the master playlist.
3958
- * This has not been canonicalized into an absolute URI. This gives us a
3959
- * consistent key for this playlist, even if redirects cause us to update
3960
- * from different origins each time.
3961
- * @property {string} absoluteMediaPlaylistUri
3962
- * The absolute media playlist URI, resolved relative to the master playlist
3963
- * and updated to reflect any redirects.
4126
+ * @property {!Array.<string>} redirectUris
4127
+ * The redirect URIs.
4128
+ * @property {function():!Array.<string>} getUris
4129
+ * The verbatim media playlist URIs, as it appeared in the master playlist.
3964
4130
  * @property {number} minTimestamp
3965
4131
  * The minimum timestamp found in the stream.
3966
4132
  * @property {number} maxTimestamp
@@ -4002,30 +4168,6 @@ shaka.hls.HlsParser.StreamInfo;
4002
4168
  shaka.hls.HlsParser.StreamInfos;
4003
4169
 
4004
4170
 
4005
- /**
4006
- * @typedef {{
4007
- * type: string,
4008
- * mimeType: string,
4009
- * codecs: string,
4010
- * language: ?string,
4011
- * height: ?string,
4012
- * width: ?string,
4013
- * channelCount: ?number,
4014
- * sampleRate: ?number
4015
- * }}
4016
- *
4017
- * @property {string} type
4018
- * @property {string} mimeType
4019
- * @property {string} codecs
4020
- * @property {?string} language
4021
- * @property {?string} height
4022
- * @property {?string} width
4023
- * @property {?number} channelCount
4024
- * @property {?number} sampleRate
4025
- */
4026
- shaka.hls.HlsParser.BasicInfo;
4027
-
4028
-
4029
4171
  /**
4030
4172
  * @const {!Object.<string, string>}
4031
4173
  * @private
@@ -4147,8 +4289,6 @@ shaka.hls.HlsParser.PresentationType_ = {
4147
4289
  LIVE: 'LIVE',
4148
4290
  };
4149
4291
 
4150
- shaka.media.ManifestParser.registerParserByExtension(
4151
- 'm3u8', () => new shaka.hls.HlsParser());
4152
4292
  shaka.media.ManifestParser.registerParserByMime(
4153
4293
  'application/x-mpegurl', () => new shaka.hls.HlsParser());
4154
4294
  shaka.media.ManifestParser.registerParserByMime(