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
package/lib/player.js CHANGED
@@ -7,8 +7,8 @@
7
7
  goog.provide('shaka.Player');
8
8
 
9
9
  goog.require('goog.asserts');
10
- goog.require('shaka.Deprecate');
11
10
  goog.require('shaka.config.AutoShowText');
11
+ goog.require('shaka.Deprecate');
12
12
  goog.require('shaka.log');
13
13
  goog.require('shaka.media.AdaptationSetCriteria');
14
14
  goog.require('shaka.media.BufferingObserver');
@@ -31,15 +31,16 @@ goog.require('shaka.media.SrcEqualsPlayhead');
31
31
  goog.require('shaka.media.StreamingEngine');
32
32
  goog.require('shaka.media.TimeRangesUtils');
33
33
  goog.require('shaka.net.NetworkingEngine');
34
- goog.require('shaka.routing.Walker');
34
+ goog.require('shaka.net.NetworkingUtils');
35
35
  goog.require('shaka.text.SimpleTextDisplayer');
36
+ goog.require('shaka.text.StubTextDisplayer');
36
37
  goog.require('shaka.text.TextEngine');
37
38
  goog.require('shaka.text.UITextDisplayer');
38
39
  goog.require('shaka.text.WebVttGenerator');
39
- goog.require('shaka.util.AbortableOperation');
40
40
  goog.require('shaka.util.BufferUtils');
41
41
  goog.require('shaka.util.CmcdManager');
42
42
  goog.require('shaka.util.ConfigUtils');
43
+ goog.require('shaka.util.Dom');
43
44
  goog.require('shaka.util.Error');
44
45
  goog.require('shaka.util.EventManager');
45
46
  goog.require('shaka.util.FakeEvent');
@@ -49,6 +50,7 @@ goog.require('shaka.util.LanguageUtils');
49
50
  goog.require('shaka.util.ManifestParserUtils');
50
51
  goog.require('shaka.util.MediaReadyState');
51
52
  goog.require('shaka.util.MimeUtils');
53
+ goog.require('shaka.util.Mutex');
52
54
  goog.require('shaka.util.ObjectUtils');
53
55
  goog.require('shaka.util.Platform');
54
56
  goog.require('shaka.util.PlayerConfiguration');
@@ -56,10 +58,8 @@ goog.require('shaka.util.PublicPromise');
56
58
  goog.require('shaka.util.Stats');
57
59
  goog.require('shaka.util.StreamUtils');
58
60
  goog.require('shaka.util.Timer');
59
- goog.require('shaka.lcevc.Dil');
61
+ goog.require('shaka.lcevc.Dec');
60
62
  goog.requireType('shaka.media.PresentationTimeline');
61
- goog.requireType('shaka.routing.Node');
62
- goog.requireType('shaka.routing.Payload');
63
63
 
64
64
 
65
65
  /**
@@ -85,18 +85,6 @@ goog.requireType('shaka.routing.Payload');
85
85
  * @exportDoc
86
86
  */
87
87
 
88
- /**
89
- * @event shaka.Player.StateIdleEvent
90
- * @description Fired when the player has stopped changing states and will
91
- * remain idle until a new state change request (e.g. <code>load</code>,
92
- * <code>attach</code>, etc.) is made.
93
- * @property {string} type
94
- * 'onstateidle'
95
- * @property {string} state
96
- * The name of the state that the player stopped in.
97
- * @exportDoc
98
- */
99
-
100
88
  /**
101
89
  * @event shaka.Player.EmsgEvent
102
90
  * @description Fired when an emsg box is found in a segment.
@@ -314,6 +302,18 @@ goog.requireType('shaka.routing.Payload');
314
302
  */
315
303
 
316
304
 
305
+ /**
306
+ * @event shaka.Player.ManifestUpdatedEvent
307
+ * @description Fired after the manifest has been updated (live streams).
308
+ * @property {string} type
309
+ * 'manifestupdated'
310
+ * @property {boolean} isLive
311
+ * True when the playlist is live. Useful to detect transition from live
312
+ * to static playlist..
313
+ * @exportDoc
314
+ */
315
+
316
+
317
317
  /**
318
318
  * @event shaka.Player.MetadataEvent
319
319
  * @description Triggers after metadata associated with the stream is found.
@@ -423,6 +423,76 @@ goog.requireType('shaka.routing.Payload');
423
423
  */
424
424
 
425
425
 
426
+ /**
427
+ * @event shaka.Player.KeyStatusChanged
428
+ * @description Fired when the key status changed.
429
+ * @property {string} type
430
+ * 'keystatuschanged'
431
+ * @exportDoc
432
+ */
433
+
434
+
435
+ /**
436
+ * @event shaka.Player.StateChanged
437
+ * @description Fired when player state is changed.
438
+ * @property {string} type
439
+ * 'statechanged'
440
+ * @property {string} newstate
441
+ * The new state.
442
+ * @exportDoc
443
+ */
444
+
445
+
446
+ /**
447
+ * @event shaka.Player.Started
448
+ * @description Fires when the content starts playing.
449
+ * Only for VoD.
450
+ * @property {string} type
451
+ * 'started'
452
+ * @exportDoc
453
+ */
454
+
455
+
456
+ /**
457
+ * @event shaka.Player.FirstQuartile
458
+ * @description Fires when the content playhead crosses first quartile.
459
+ * Only for VoD.
460
+ * @property {string} type
461
+ * 'firstquartile'
462
+ * @exportDoc
463
+ */
464
+
465
+
466
+ /**
467
+ * @event shaka.Player.Midpoint
468
+ * @description Fires when the content playhead crosses midpoint.
469
+ * Only for VoD.
470
+ * @property {string} type
471
+ * 'midpoint'
472
+ * @exportDoc
473
+ */
474
+
475
+
476
+ /**
477
+ * @event shaka.Player.ThirdQuartile
478
+ * @description Fires when the content playhead crosses third quartile.
479
+ * Only for VoD.
480
+ * @property {string} type
481
+ * 'thirdquartile'
482
+ * @exportDoc
483
+ */
484
+
485
+
486
+ /**
487
+ * @event shaka.Player.Complete
488
+ * @description Fires when the content completes playing.
489
+ * Only for VoD.
490
+ * @property {string} type
491
+ * 'complete'
492
+ * @exportDoc
493
+ */
494
+
495
+
426
496
  /**
427
497
  * @summary The main player object for Shaka Player.
428
498
  *
@@ -491,6 +561,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
491
561
  /** @private {shaka.media.Playhead} */
492
562
  this.playhead_ = null;
493
563
 
564
+ /**
565
+ * Incremented whenever a top-level operation (load, attach, etc) is
566
+ * performed.
567
+ * Used to determine if a load operation has been interrupted.
568
+ * @private {number}
569
+ */
570
+ this.operationId_ = 0;
571
+
572
+ /** @private {!shaka.util.Mutex} */
573
+ this.mutex_ = new shaka.util.Mutex();
574
+
494
575
  /**
495
576
  * The playhead observers are used to monitor the position of the playhead
496
577
  * and some other source of data (e.g. buffered content), and raise events.
@@ -508,12 +589,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
508
589
  */
509
590
  this.playRateController_ = null;
510
591
 
511
- // We use the buffering observer and timer to track when we move from having
512
- // enough buffered content to not enough. They only exist when content has
513
- // been loaded and are not re-used between loads.
514
- /** @private {shaka.util.Timer} */
515
- this.bufferPoller_ = null;
516
-
517
592
  /** @private {shaka.media.BufferingObserver} */
518
593
  this.bufferObserver_ = null;
519
594
 
@@ -528,9 +603,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
528
603
  /** @private {?HTMLCanvasElement} */
529
604
  this.lcevcCanvas_ = null;
530
605
 
531
- // This is the LCEVC Dil object to decode LCEVC.
532
- /** @private {?shaka.lcevc.Dil} */
533
- this.lcevcDil_ = null;
606
+ // This is the LCEVC Decoder object to decode LCEVC.
607
+ /** @private {?shaka.lcevc.Dec} */
608
+ this.lcevcDec_ = null;
534
609
 
535
610
  /** @private {shaka.media.QualityObserver} */
536
611
  this.qualityObserver_ = null;
@@ -550,6 +625,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
550
625
  /** @private {?string} */
551
626
  this.assetUri_ = null;
552
627
 
628
+ /** @private {?number} */
629
+ this.startTime_ = null;
630
+
553
631
  /** @private {boolean} */
554
632
  this.fullyLoaded_ = false;
555
633
 
@@ -569,6 +647,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
569
647
  */
570
648
  this.nextExternalStreamId_ = 1e9;
571
649
 
650
+ /** @private {!Array.<shaka.extern.Stream>} */
651
+ this.externalSrcEqualsThumbnailsStreams_ = [];
652
+
653
+ /** @private {number} */
654
+ this.completionPercent_ = NaN;
655
+
572
656
  /** @private {?shaka.extern.PlayerConfiguration} */
573
657
  this.config_ = this.defaultConfig_();
574
658
 
@@ -591,7 +675,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
591
675
  this.config_.preferredAudioLanguage,
592
676
  this.config_.preferredVariantRole,
593
677
  this.config_.preferredAudioChannelCount,
594
- this.config_.preferredVideoHdrLevel);
678
+ this.config_.preferredVideoHdrLevel,
679
+ this.config_.preferredVideoLayout,
680
+ this.config_.preferredAudioLabel,
681
+ this.config_.mediaSource.codecSwitchingStrategy,
682
+ this.config_.manifest.dash.enableAudioGroups);
595
683
 
596
684
  /** @private {string} */
597
685
  this.currentTextLanguage_ = this.config_.preferredTextLanguage;
@@ -605,15 +693,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
605
693
  /** @private {!Array.<function():(!Promise|undefined)>} */
606
694
  this.cleanupOnUnload_ = [];
607
695
 
608
- /**
609
- * This playback start position will be used when
610
- * <code>updateStartTime()</code> has been called to provide an updated
611
- * start position during the media loading process.
612
- *
613
- * @private {?number}
614
- */
615
- this.updatedStartTime_ = null;
616
-
617
696
  if (dependencyInjector) {
618
697
  dependencyInjector(this);
619
698
  }
@@ -639,117 +718,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
639
718
  this.retryStreaming();
640
719
  });
641
720
 
642
- /** @private {shaka.routing.Node} */
643
- this.detachNode_ = {name: 'detach'};
644
- /** @private {shaka.routing.Node} */
645
- this.attachNode_ = {name: 'attach'};
646
- /** @private {shaka.routing.Node} */
647
- this.unloadNode_ = {name: 'unload'};
648
- /** @private {shaka.routing.Node} */
649
- this.parserNode_ = {name: 'manifest-parser'};
650
- /** @private {shaka.routing.Node} */
651
- this.manifestNode_ = {name: 'manifest'};
652
- /** @private {shaka.routing.Node} */
653
- this.mediaSourceNode_ = {name: 'media-source'};
654
- /** @private {shaka.routing.Node} */
655
- this.drmNode_ = {name: 'drm-engine'};
656
- /** @private {shaka.routing.Node} */
657
- this.loadNode_ = {name: 'load'};
658
- /** @private {shaka.routing.Node} */
659
- this.srcEqualsDrmNode_ = {name: 'src-equals-drm-engine'};
660
- /** @private {shaka.routing.Node} */
661
- this.srcEqualsNode_ = {name: 'src-equals'};
662
-
663
- const AbortableOperation = shaka.util.AbortableOperation;
664
-
665
- const actions = new Map();
666
- actions.set(this.attachNode_, (has, wants) => {
667
- return AbortableOperation.notAbortable(this.onAttach_(has, wants));
668
- });
669
- actions.set(this.detachNode_, (has, wants) => {
670
- return AbortableOperation.notAbortable(this.onDetach_(has, wants));
671
- });
672
- actions.set(this.unloadNode_, (has, wants) => {
673
- return AbortableOperation.notAbortable(this.onUnload_(has, wants));
674
- });
675
- actions.set(this.mediaSourceNode_, (has, wants) => {
676
- const p = this.onInitializeMediaSourceEngine_(has, wants);
677
- return AbortableOperation.notAbortable(p);
678
- });
679
- actions.set(this.parserNode_, (has, wants) => {
680
- const p = this.onInitializeParser_(has, wants);
681
- return AbortableOperation.notAbortable(p);
682
- });
683
- actions.set(this.manifestNode_, (has, wants) => {
684
- // This action is actually abortable, so unlike the other callbacks, this
685
- // one will return an abortable operation.
686
- return this.onParseManifest_(has, wants);
687
- });
688
- actions.set(this.drmNode_, (has, wants) => {
689
- const p = this.onInitializeDrm_(has, wants);
690
- return AbortableOperation.notAbortable(p);
691
- });
692
- actions.set(this.loadNode_, (has, wants) => {
693
- return AbortableOperation.notAbortable(this.onLoad_(has, wants));
694
- });
695
-
696
- actions.set(this.srcEqualsDrmNode_, (has, wants) => {
697
- const p = this.onInitializeSrcEqualsDrm_(has, wants);
698
- return AbortableOperation.notAbortable(p);
699
- });
700
- actions.set(this.srcEqualsNode_, (has, wants) => {
701
- return this.onSrcEquals_(has, wants);
702
- });
703
-
704
- /** @private {shaka.routing.Walker.Implementation} */
705
- const walkerImplementation = {
706
- getNext: (at, has, goingTo, wants) => {
707
- return this.getNextStep_(at, has, goingTo, wants);
708
- },
709
- enterNode: (node, has, wants) => {
710
- this.dispatchEvent(this.makeEvent_(
711
- /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
712
- /* data= */ (new Map()).set('state', node.name)));
713
-
714
- const action = actions.get(node);
715
- return action(has, wants);
716
- },
717
- handleError: async (has, error) => {
718
- shaka.log.warning('The walker saw an error:');
719
- if (error instanceof shaka.util.Error) {
720
- shaka.log.warning('Error Code:', error.code);
721
- } else {
722
- shaka.log.warning('Error Message:', error.message);
723
- shaka.log.warning('Error Stack:', error.stack);
724
- }
725
-
726
- // Regardless of what state we were in, if there is an error, we unload.
727
- // This ensures that any initialized system will be torn-down and we
728
- // will go back to a safe foundation. We assume that the media element
729
- // is always safe to use after an error.
730
- await this.onUnload_(has, shaka.Player.createEmptyPayload_());
731
-
732
- // There are only two nodes that come before we start loading content,
733
- // attach and detach. If we have a media element, it means we were
734
- // attached to the element, and we can safely return to the attach state
735
- // (we assume that the video element is always re-usable). We favor
736
- // returning to the attach node since it means that the app won't need
737
- // to re-attach if it saw an error.
738
- return has.mediaElement ? this.attachNode_ : this.detachNode_;
739
- },
740
- onIdle: (node) => {
741
- this.dispatchEvent(this.makeEvent_(
742
- /* name= */ shaka.util.FakeEvent.EventName.OnStateIdle,
743
- /* data= */ (new Map()).set('state', node.name)));
744
- },
745
- };
746
-
747
- /** @private {shaka.routing.Walker} */
748
- this.walker_ = new shaka.routing.Walker(
749
- this.detachNode_,
750
- shaka.Player.createEmptyPayload_(),
751
- walkerImplementation);
752
-
753
721
  /** @private {shaka.util.Timer} */
754
722
  this.checkVariantsTimer_ =
755
723
  new shaka.util.Timer(() => this.checkVariants_());
@@ -758,50 +726,53 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
758
726
  // the LAST thing we do in the constructor because conceptually it relies on
759
727
  // player having been initialized.
760
728
  if (mediaElement) {
729
+ shaka.Deprecate.deprecateFeature(5,
730
+ 'Player',
731
+ 'Please migrate from initializing Player with a mediaElement; ' +
732
+ 'use the attach method instead.');
761
733
  this.attach(mediaElement, /* initializeMediaSource= */ true);
762
734
  }
763
735
  }
764
736
 
765
737
  /**
766
- * Create a shaka.lcevc.Dil object
738
+ * Create a shaka.lcevc.Dec object
767
739
  * @param {shaka.extern.LcevcConfiguration} config
768
740
  * @private
769
741
  */
770
- createLcevcDil_(config) {
771
- if (this.lcevcDil_ == null) {
772
- this.lcevcDil_ = new shaka.lcevc.Dil(
742
+ createLcevcDec_(config) {
743
+ if (this.lcevcDec_ == null) {
744
+ this.lcevcDec_ = new shaka.lcevc.Dec(
773
745
  /** @type {HTMLVideoElement} */ (this.video_),
774
746
  this.lcevcCanvas_,
775
747
  config,
776
748
  );
777
749
  if (this.mediaSourceEngine_) {
778
- this.mediaSourceEngine_.updateLcevcDil(this.lcevcDil_);
750
+ this.mediaSourceEngine_.updateLcevcDec(this.lcevcDec_);
779
751
  }
780
752
  }
781
753
  }
782
754
 
783
755
  /**
784
- * Close a shaka.lcevc.Dil object if present and hide the canvas.
756
+ * Close a shaka.lcevc.Dec object if present and hide the canvas.
785
757
  * @private
786
758
  */
787
- closeLcevcDil_() {
788
- if (this.lcevcDil_ != null) {
789
- this.lcevcDil_.release();
790
- this.lcevcDil_ = null;
759
+ closeLcevcDec_() {
760
+ if (this.lcevcDec_ != null) {
761
+ this.lcevcDec_.hideCanvas();
762
+ this.lcevcDec_.release();
763
+ this.lcevcDec_ = null;
791
764
  }
792
765
  }
793
766
 
794
767
  /**
795
- * Setup shaka.lcevc.Dil object
768
+ * Setup shaka.lcevc.Dec object
796
769
  * @param {?shaka.extern.PlayerConfiguration} config
797
770
  * @private
798
771
  */
799
772
  setupLcevc_(config) {
800
773
  if (config.lcevc.enabled) {
801
774
  const tracks = this.getVariantTracks();
802
- if (tracks && tracks[0] &&
803
- tracks[0].videoMimeType ==
804
- shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_['ts']) {
775
+ if (tracks && tracks[0] && tracks[0].videoMimeType == 'video/mp2t') {
805
776
  const edge = shaka.util.Platform.isEdge() ||
806
777
  shaka.util.Platform.isLegacyEdge();
807
778
  if (edge) {
@@ -815,10 +786,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
815
786
  }
816
787
  }
817
788
  }
818
- this.closeLcevcDil_();
819
- this.createLcevcDil_(config.lcevc);
789
+ this.closeLcevcDec_();
790
+ this.createLcevcDec_(config.lcevc);
820
791
  } else {
821
- this.closeLcevcDil_();
792
+ this.closeLcevcDec_();
822
793
  }
823
794
  }
824
795
 
@@ -844,51 +815,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
844
815
  return;
845
816
  }
846
817
 
847
- // If LCEVC Dil exists close it.
848
- this.closeLcevcDil_();
818
+ // If LCEVC Decoder exists close it.
819
+ this.closeLcevcDec_();
820
+
821
+ const detachPromise = this.detach();
849
822
 
850
823
  // Mark as "dead". This should stop external-facing calls from changing our
851
824
  // internal state any more. This will stop calls to |attach|, |detach|, etc.
852
825
  // from interrupting our final move to the detached state.
853
826
  this.loadMode_ = shaka.Player.LoadMode.DESTROYED;
854
827
 
855
- // Because we have set |loadMode_| to |DESTROYED| we can't call |detach|. We
856
- // must talk to |this.walker_| directly.
857
- const events = this.walker_.startNewRoute((currentPayload) => {
858
- return {
859
- node: this.detachNode_,
860
- payload: shaka.Player.createEmptyPayload_(),
861
- interruptible: false,
862
- };
863
- });
864
-
865
- // Wait until the detach has finished so that we don't interrupt it by
866
- // calling |destroy| on |this.walker_|. To avoid failing here, we always
867
- // resolve the promise.
868
- await new Promise((resolve) => {
869
- events.onStart = () => {
870
- shaka.log.info('Preparing to destroy walker...');
871
- };
872
- events.onEnd = () => {
873
- resolve();
874
- };
875
- events.onCancel = () => {
876
- goog.asserts.assert(false,
877
- 'Our final detach call should never be cancelled.');
878
- resolve();
879
- };
880
- events.onError = () => {
881
- goog.asserts.assert(false,
882
- 'Our final detach call should never see an error');
883
- resolve();
884
- };
885
- events.onSkip = () => {
886
- goog.asserts.assert(false,
887
- 'Our final detach call should never be skipped');
888
- resolve();
889
- };
890
- });
891
- await this.walker_.destroy();
828
+ await detachPromise;
892
829
 
893
830
  // Tear-down the event managers to ensure handlers stop firing.
894
831
  if (this.globalEventManager_) {
@@ -974,10 +911,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
974
911
  return false;
975
912
  }
976
913
 
977
- // We do not support iOS 9, 10, 11 or 12, nor those same versions of
978
- // desktop Safari.
979
914
  const safariVersion = shaka.util.Platform.safariVersion();
980
- if (safariVersion && safariVersion < 13) {
915
+ if (safariVersion && safariVersion < 9) {
981
916
  return false;
982
917
  }
983
918
 
@@ -1036,56 +971,60 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1036
971
  }
1037
972
 
1038
973
  /**
1039
- * Tell the player to use <code>mediaElement</code> for all <code>load</code>
1040
- * requests until <code>detach</code> or <code>destroy</code> are called.
1041
- *
1042
- * <p>
1043
- * Calling <code>attach</code> with <code>initializedMediaSource=true</code>
1044
- * will tell the player to take the initial load step and initialize media
1045
- * source.
1046
- *
1047
- * <p>
1048
- * Calls to <code>attach</code> will interrupt any in-progress calls to
1049
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1050
- * <code>detach</code>, or <code>unload</code>.
974
+ * Makes a fires an event corresponding to entering a state of the loading
975
+ * process.
976
+ * @param {string} nodeName
977
+ * @private
978
+ */
979
+ makeStateChangeEvent_(nodeName) {
980
+ this.dispatchEvent(this.makeEvent_(
981
+ /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
982
+ /* data= */ (new Map()).set('state', nodeName)));
983
+ }
984
+
985
+ /**
986
+ * Attaches the player to a media element.
987
+ * If the player was already attached to a media element, first detaches from
988
+ * that media element.
1051
989
  *
1052
990
  * @param {!HTMLMediaElement} mediaElement
1053
991
  * @param {boolean=} initializeMediaSource
1054
992
  * @return {!Promise}
1055
993
  * @export
1056
994
  */
1057
- attach(mediaElement, initializeMediaSource = true) {
995
+ async attach(mediaElement, initializeMediaSource = true) {
1058
996
  // Do not allow the player to be used after |destroy| is called.
1059
997
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1060
- return Promise.reject(this.createAbortLoadError_());
998
+ throw this.createAbortLoadError_();
1061
999
  }
1062
1000
 
1063
- const payload = shaka.Player.createEmptyPayload_();
1064
- payload.mediaElement = mediaElement;
1065
-
1066
- // If the platform does not support media source, we will never want to
1067
- // initialize media source.
1068
- if (!shaka.util.Platform.supportsMediaSource()) {
1069
- initializeMediaSource = false;
1001
+ if (this.video_ && this.video_ != mediaElement) {
1002
+ await this.detach();
1070
1003
  }
1071
1004
 
1072
- const destination = initializeMediaSource ?
1073
- this.mediaSourceNode_ :
1074
- this.attachNode_;
1005
+ if (await this.atomicOperationAcquireMutex_('attach')) {
1006
+ return;
1007
+ }
1075
1008
 
1076
- // Do not allow this route to be interrupted because calls after this attach
1077
- // call will depend on the media element being attached.
1078
- const events = this.walker_.startNewRoute((currentPayload) => {
1079
- return {
1080
- node: destination,
1081
- payload: payload,
1082
- interruptible: false,
1083
- };
1084
- });
1009
+ try {
1010
+ this.makeStateChangeEvent_('attach');
1085
1011
 
1086
- // List to the events that can occur with our request.
1087
- events.onStart = () => shaka.log.info('Starting attach...');
1088
- return this.wrapWalkerListenersWithPromise_(events);
1012
+ const onError = (error) => this.onVideoError_(error);
1013
+ this.attachEventManager_.listen(mediaElement, 'error', onError);
1014
+ this.video_ = mediaElement;
1015
+
1016
+ // Only initialize media source if the platform supports it.
1017
+ if (initializeMediaSource &&
1018
+ shaka.util.Platform.supportsMediaSource() &&
1019
+ !this.mediaSourceEngine_) {
1020
+ await this.initializeMediaSourceEngineInner_();
1021
+ }
1022
+ } catch (error) {
1023
+ await this.detach();
1024
+ throw error;
1025
+ } finally {
1026
+ this.mutex_.release();
1027
+ }
1089
1028
  }
1090
1029
 
1091
1030
 
@@ -1101,129 +1040,246 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1101
1040
  }
1102
1041
 
1103
1042
  /**
1104
- * Tell the player to stop using its current media element. If the player is:
1105
- * <ul>
1106
- * <li>detached, this will do nothing,
1107
- * <li>attached, this will release the media element,
1108
- * <li>loading, this will abort loading, unload, and release the media
1109
- * element,
1110
- * <li>playing content, this will stop playback, unload, and release the
1111
- * media element.
1112
- * </ul>
1043
+ * Detach the player from the current media element. Leaves the player in a
1044
+ * state where it cannot play media, until it has been attached to something
1045
+ * else.
1113
1046
  *
1114
- * <p>
1115
- * Calls to <code>detach</code> will interrupt any in-progress calls to
1116
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1117
- * <code>detach</code>, or <code>unload</code>.
1047
+ * @param {boolean=} keepAdManager
1118
1048
  *
1119
1049
  * @return {!Promise}
1120
1050
  * @export
1121
1051
  */
1122
- detach() {
1052
+ async detach(keepAdManager = false) {
1123
1053
  // Do not allow the player to be used after |destroy| is called.
1124
1054
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1125
- return Promise.reject(this.createAbortLoadError_());
1055
+ throw this.createAbortLoadError_();
1126
1056
  }
1127
1057
 
1128
- // Tell the walker to go "detached", but do not allow it to be interrupted.
1129
- // If it could be interrupted it means that our media element could fall out
1130
- // of sync.
1131
- const events = this.walker_.startNewRoute((currentPayload) => {
1132
- return {
1133
- node: this.detachNode_,
1134
- payload: shaka.Player.createEmptyPayload_(),
1135
- interruptible: false,
1136
- };
1137
- });
1058
+ await this.unload(/* initializeMediaSource= */ false, keepAdManager);
1059
+
1060
+ if (await this.atomicOperationAcquireMutex_('detach')) {
1061
+ return;
1062
+ }
1138
1063
 
1139
- events.onStart = () => shaka.log.info('Starting detach...');
1140
- return this.wrapWalkerListenersWithPromise_(events);
1064
+ try {
1065
+ // If we were going from "detached" to "detached" we wouldn't have
1066
+ // a media element to detach from.
1067
+ if (this.video_) {
1068
+ this.attachEventManager_.removeAll();
1069
+ this.video_ = null;
1070
+ }
1071
+
1072
+ this.makeStateChangeEvent_('detach');
1073
+
1074
+ if (this.adManager_ && !keepAdManager) {
1075
+ // The ad manager is specific to the video, so detach it too.
1076
+ this.adManager_.release();
1077
+ }
1078
+ } finally {
1079
+ this.mutex_.release();
1080
+ }
1141
1081
  }
1142
1082
 
1143
1083
  /**
1144
- * Tell the player to either return to:
1145
- * <ul>
1146
- * <li>detached (when it does not have a media element),
1147
- * <li>attached (when it has a media element and
1148
- * <code>initializedMediaSource=false</code>)
1149
- * <li>media source initialized (when it has a media element and
1150
- * <code>initializedMediaSource=true</code>)
1151
- * </ul>
1152
- *
1153
- * <p>
1154
- * Calls to <code>unload</code> will interrupt any in-progress calls to
1155
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1156
- * <code>detach</code>, or <code>unload</code>.
1084
+ * Tries to acquire the mutex, and then returns if the operation should end
1085
+ * early due to someone else starting a mutex-acquiring operation.
1086
+ * Meant for operations that can't be interrupted midway through (e.g.
1087
+ * everything but load).
1088
+ * @param {string} mutexIdentifier
1089
+ * @return {!Promise.<boolean>} endEarly If false, the calling context will
1090
+ * need to release the mutex.
1091
+ * @private
1092
+ */
1093
+ async atomicOperationAcquireMutex_(mutexIdentifier) {
1094
+ const operationId = ++this.operationId_;
1095
+ await this.mutex_.acquire(mutexIdentifier);
1096
+ if (operationId != this.operationId_) {
1097
+ this.mutex_.release();
1098
+ return true;
1099
+ }
1100
+ return false;
1101
+ }
1102
+
1103
+ /**
1104
+ * Unloads the currently playing stream, if any.
1157
1105
  *
1158
1106
  * @param {boolean=} initializeMediaSource
1107
+ * @param {boolean=} keepAdManager
1159
1108
  * @return {!Promise}
1160
1109
  * @export
1161
1110
  */
1162
- unload(initializeMediaSource = true) {
1163
- // Do not allow the player to be used after |destroy| is called.
1164
- if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1165
- return Promise.reject(this.createAbortLoadError_());
1111
+ async unload(initializeMediaSource = true, keepAdManager = false) {
1112
+ // Set the load mode to unload right away so that all the public methods
1113
+ // will stop using the internal components. We need to make sure that we
1114
+ // are not overriding the destroyed state because we will unload when we are
1115
+ // destroying the player.
1116
+ if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
1117
+ this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
1166
1118
  }
1167
1119
 
1168
- this.fullyLoaded_ = false;
1169
-
1170
- // If the platform does not support media source, we will never want to
1171
- // initialize media source.
1172
- if (!shaka.util.Platform.supportsMediaSource()) {
1173
- initializeMediaSource = false;
1120
+ if (await this.atomicOperationAcquireMutex_('unload')) {
1121
+ return;
1174
1122
  }
1175
1123
 
1176
- // If LCEVC Dil exists close it.
1177
- this.closeLcevcDil_();
1124
+ try {
1125
+ this.fullyLoaded_ = false;
1126
+ this.makeStateChangeEvent_('unload');
1178
1127
 
1179
- // Since we are going either to attached or detached (through unloaded), we
1180
- // can't allow it to be interrupted or else we could lose track of what
1181
- // media element we are suppose to use.
1182
- //
1183
- // Using the current payload, we can determine which node we want to go to.
1184
- // If we have a media element, we want to go back to attached. If we have no
1185
- // media element, we want to go back to detached.
1186
- const payload = shaka.Player.createEmptyPayload_();
1187
-
1188
- const events = this.walker_.startNewRoute((currentPayload) => {
1189
- // When someone calls |unload| we can either be before attached or
1190
- // detached (there is nothing stopping someone from calling |detach| when
1191
- // we are already detached).
1192
- //
1193
- // If we are attached to the correct element, we can tear down the
1194
- // previous playback components and go to the attached media source node
1195
- // depending on whether or not the caller wants to pre-init media source.
1128
+ // If the platform does not support media source, we will never want to
1129
+ // initialize media source.
1130
+ if (initializeMediaSource && !shaka.util.Platform.supportsMediaSource()) {
1131
+ initializeMediaSource = false;
1132
+ }
1133
+
1134
+ // If LCEVC Decoder exists close it.
1135
+ this.closeLcevcDec_();
1136
+
1137
+ // Run any general cleanup tasks now. This should be here at the top,
1138
+ // right after setting loadMode_, so that internal components still exist
1139
+ // as they did when the cleanup tasks were registered in the array.
1140
+ const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb());
1141
+ this.cleanupOnUnload_ = [];
1142
+ await Promise.all(cleanupTasks);
1143
+
1144
+ // Dispatch the unloading event.
1145
+ this.dispatchEvent(
1146
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1147
+
1148
+ // Release the region timeline, which is created when parsing the
1149
+ // manifest.
1150
+ if (this.regionTimeline_) {
1151
+ this.regionTimeline_.release();
1152
+ this.regionTimeline_ = null;
1153
+ }
1154
+
1155
+ // In most cases we should have a media element. The one exception would
1156
+ // be if there was an error and we, by chance, did not have a media
1157
+ // element.
1158
+ if (this.video_) {
1159
+ this.loadEventManager_.removeAll();
1160
+ }
1161
+
1162
+ // Stop the variant checker timer
1163
+ this.checkVariantsTimer_.stop();
1164
+
1165
+ // Some observers use some playback components, shutting down the
1166
+ // observers first ensures that they don't try to use the playback
1167
+ // components mid-destroy.
1168
+ if (this.playheadObservers_) {
1169
+ this.playheadObservers_.release();
1170
+ this.playheadObservers_ = null;
1171
+ }
1172
+
1173
+ // Stop the parser early. Since it is at the start of the pipeline, it
1174
+ // should be start early to avoid is pushing new data downstream.
1175
+ if (this.parser_) {
1176
+ await this.parser_.stop();
1177
+ this.parser_ = null;
1178
+ this.parserFactory_ = null;
1179
+ }
1180
+
1181
+ // Abr Manager will tell streaming engine what to do, so we need to stop
1182
+ // it before we destroy streaming engine. Unlike with the other
1183
+ // components, we do not release the instance, we will reuse it in later
1184
+ // loads.
1185
+ if (this.abrManager_) {
1186
+ await this.abrManager_.stop();
1187
+ }
1188
+
1189
+ // Streaming engine will push new data to media source engine, so we need
1190
+ // to shut it down before destroy media source engine.
1191
+ if (this.streamingEngine_) {
1192
+ await this.streamingEngine_.destroy();
1193
+ this.streamingEngine_ = null;
1194
+ }
1195
+
1196
+ if (this.playRateController_) {
1197
+ this.playRateController_.release();
1198
+ this.playRateController_ = null;
1199
+ }
1200
+
1201
+ // Playhead is used by StreamingEngine, so we can't destroy this until
1202
+ // after StreamingEngine has stopped.
1203
+ if (this.playhead_) {
1204
+ this.playhead_.release();
1205
+ this.playhead_ = null;
1206
+ }
1207
+
1208
+ // Media source engine holds onto the media element, and in order to
1209
+ // detach the media keys (with drm engine), we need to break the
1210
+ // connection between media source engine and the media element.
1211
+ if (this.mediaSourceEngine_) {
1212
+ await this.mediaSourceEngine_.destroy();
1213
+ this.mediaSourceEngine_ = null;
1214
+ }
1215
+
1216
+ if (this.adManager_ && !keepAdManager) {
1217
+ this.adManager_.onAssetUnload();
1218
+ }
1219
+
1220
+ if (this.video_) {
1221
+ // Remove all track nodes
1222
+ shaka.util.Dom.removeAllChildren(this.video_);
1223
+ }
1224
+
1225
+ // In order to unload a media element, we need to remove the src attribute
1226
+ // and then load again. When we destroy media source engine, this will be
1227
+ // done for us, but for src=, we need to do it here.
1196
1228
  //
1197
- // If we don't have a media element, we assume that we are already at the
1198
- // detached node - but only the walker knows that. To ensure we are
1199
- // actually there, we tell the walker to go to detach. While this is
1200
- // technically unnecessary, it ensures that we are in the state we want
1201
- // to be in and ready for the next request.
1202
- let destination = null;
1203
-
1204
- if (currentPayload.mediaElement && initializeMediaSource) {
1205
- destination = this.mediaSourceNode_;
1206
- } else if (currentPayload.mediaElement) {
1207
- destination = this.attachNode_;
1208
- } else {
1209
- destination = this.detachNode_;
1229
+ // DrmEngine requires this to be done before we destroy DrmEngine itself.
1230
+ if (this.video_ && this.video_.src) {
1231
+ // TODO: Investigate this more. Only reproduces on Firefox 69.
1232
+ // Introduce a delay before detaching the video source. We are seeing
1233
+ // spurious Promise rejections involving an AbortError in our tests
1234
+ // otherwise.
1235
+ await new Promise(
1236
+ (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
1237
+
1238
+ this.video_.removeAttribute('src');
1239
+ this.video_.load();
1210
1240
  }
1211
1241
 
1212
- goog.asserts.assert(destination, 'We should have picked a destination.');
1242
+ if (this.drmEngine_) {
1243
+ await this.drmEngine_.destroy();
1244
+ this.drmEngine_ = null;
1245
+ }
1213
1246
 
1214
- // Copy over the media element because we want to keep using the same
1215
- // element - the other values don't matter.
1216
- payload.mediaElement = currentPayload.mediaElement;
1247
+ this.assetUri_ = null;
1248
+ this.bufferObserver_ = null;
1217
1249
 
1218
- return {
1219
- node: destination,
1220
- payload: payload,
1221
- interruptible: false,
1222
- };
1223
- });
1250
+ if (this.manifest_) {
1251
+ for (const variant of this.manifest_.variants) {
1252
+ for (const stream of [variant.audio, variant.video]) {
1253
+ if (stream && stream.segmentIndex) {
1254
+ stream.segmentIndex.release();
1255
+ }
1256
+ }
1257
+ }
1258
+ for (const stream of this.manifest_.textStreams) {
1259
+ if (stream.segmentIndex) {
1260
+ stream.segmentIndex.release();
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ this.manifest_ = null;
1266
+ this.stats_ = new shaka.util.Stats(); // Replace with a clean object.
1267
+ this.lastTextFactory_ = null;
1268
+
1269
+ this.externalSrcEqualsThumbnailsStreams_ = [];
1224
1270
 
1225
- events.onStart = () => shaka.log.info('Starting unload...');
1226
- return this.wrapWalkerListenersWithPromise_(events);
1271
+ this.completionPercent_ = NaN;
1272
+
1273
+ // Make sure that the app knows of the new buffering state.
1274
+ this.updateBufferState_();
1275
+ } finally {
1276
+ this.mutex_.release();
1277
+ }
1278
+
1279
+ if (initializeMediaSource && shaka.util.Platform.supportsMediaSource() &&
1280
+ !this.mediaSourceEngine_) {
1281
+ await this.initializeMediaSourceEngineInner_();
1282
+ }
1227
1283
  }
1228
1284
 
1229
1285
  /**
@@ -1236,120 +1292,164 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1236
1292
  * @export
1237
1293
  */
1238
1294
  updateStartTime(startTime) {
1239
- this.updatedStartTime_ = startTime;
1295
+ this.startTime_ = startTime;
1240
1296
  }
1241
1297
 
1242
1298
  /**
1243
- * Tell the player to load the content at <code>assetUri</code> and start
1244
- * playback at <code>startTime</code>. Before calling <code>load</code>,
1245
- * a call to <code>attach</code> must have succeeded.
1246
- *
1247
- * <p>
1248
- * Calls to <code>load</code> will interrupt any in-progress calls to
1249
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1250
- * <code>detach</code>, or <code>unload</code>.
1299
+ * Loads a new stream.
1300
+ * If another stream was already playing, first unloads that stream.
1251
1301
  *
1252
1302
  * @param {string} assetUri
1253
1303
  * @param {?number=} startTime
1254
1304
  * When <code>startTime</code> is <code>null</code> or
1255
1305
  * <code>undefined</code>, playback will start at the default start time (0
1256
1306
  * for VOD and liveEdge for LIVE).
1257
- * @param {string=} mimeType
1307
+ * @param {?string=} mimeType
1258
1308
  * @return {!Promise}
1259
1309
  * @export
1260
1310
  */
1261
- load(assetUri, startTime, mimeType) {
1262
- this.updatedStartTime_ = null;
1263
-
1264
- this.fullyLoaded_ = false;
1265
-
1311
+ async load(assetUri, startTime = null, mimeType) {
1266
1312
  // Do not allow the player to be used after |destroy| is called.
1267
1313
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1268
- return Promise.reject(this.createAbortLoadError_());
1314
+ throw this.createAbortLoadError_();
1269
1315
  }
1270
1316
 
1271
- // We dispatch the loading event when someone calls |load| because we want
1272
- // to surface the user intent.
1273
- this.dispatchEvent(this.makeEvent_(shaka.util.FakeEvent.EventName.Loading));
1317
+ // Quickly acquire the mutex, so this will wait for other top-level
1318
+ // operations.
1319
+ await this.mutex_.acquire('load');
1320
+ this.mutex_.release();
1274
1321
 
1275
- // Right away we know what the asset uri and start-of-load time are. We will
1276
- // fill-in the rest of the information later.
1277
- const payload = shaka.Player.createEmptyPayload_();
1278
- payload.uri = assetUri;
1279
- payload.startTimeOfLoad = Date.now() / 1000;
1280
- if (mimeType) {
1281
- payload.mimeType = mimeType;
1322
+ if (!this.video_) {
1323
+ throw new shaka.util.Error(
1324
+ shaka.util.Error.Severity.CRITICAL,
1325
+ shaka.util.Error.Category.PLAYER,
1326
+ shaka.util.Error.Code.NO_VIDEO_ELEMENT);
1282
1327
  }
1283
1328
 
1284
- // Because we allow |startTime| to be optional, it means that it will be
1285
- // |undefined| when not provided. This means that we need to re-map
1286
- // |undefined| to |null| while preserving |0| as a meaningful value.
1287
- if (startTime !== undefined) {
1288
- payload.startTime = startTime;
1329
+ if (this.assetUri_) {
1330
+ await this.unload(/* initializeMediaSource= */ false);
1289
1331
  }
1290
1332
 
1291
- // TODO: Refactor to determine whether it's a manifest or not, and whether
1292
- // or not we can play it. Then we could return a better error than
1293
- // UNABLE_TO_GUESS_MANIFEST_TYPE for WebM in Safari.
1294
- const useSrcEquals = this.shouldUseSrcEquals_(payload);
1295
- const destination = useSrcEquals ? this.srcEqualsNode_ : this.loadNode_;
1333
+ // Add a mechanism to detect if the load process has been interrupted by a
1334
+ // call to another top-level operation (unload, load, etc).
1335
+ const operationId = ++this.operationId_;
1336
+ const detectInterruption = () => {
1337
+ if (this.operationId_ != operationId) {
1338
+ throw this.createAbortLoadError_();
1339
+ }
1340
+ };
1296
1341
 
1297
- // Allow this request to be interrupted, this will allow other requests to
1298
- // cancel a load and quickly start a new load.
1299
- const events = this.walker_.startNewRoute((currentPayload) => {
1300
- if (currentPayload.mediaElement == null) {
1301
- // Because we return null, this "new route" will not be used.
1302
- return null;
1342
+ /**
1343
+ * Wraps a given operation with mutex.acquire and mutex.release, along with
1344
+ * calls to detectInterruption, to catch any other top-level calls happening
1345
+ * while waiting for the mutex.
1346
+ * @param {function():!Promise} operation
1347
+ * @param {string} mutexIdentifier
1348
+ * @return {!Promise}
1349
+ */
1350
+ const mutexWrapOperation = async (operation, mutexIdentifier) => {
1351
+ try {
1352
+ await this.mutex_.acquire(mutexIdentifier);
1353
+ detectInterruption();
1354
+ await operation();
1355
+ detectInterruption();
1356
+ } finally {
1357
+ this.mutex_.release();
1303
1358
  }
1359
+ };
1304
1360
 
1305
- // Keep using whatever media element we have right now.
1306
- payload.mediaElement = currentPayload.mediaElement;
1361
+ try {
1362
+ this.startTime_ = startTime;
1363
+ this.fullyLoaded_ = false;
1307
1364
 
1308
- return {
1309
- node: destination,
1310
- payload: payload,
1311
- interruptible: true,
1312
- };
1313
- });
1365
+ // We dispatch the loading event when someone calls |load| because we want
1366
+ // to surface the user intent.
1367
+ this.dispatchEvent(this.makeEvent_(
1368
+ shaka.util.FakeEvent.EventName.Loading));
1314
1369
 
1315
- // Stats are for a single playback/load session. Stats must be initialized
1316
- // before we allow calls to |updateStateHistory|.
1317
- this.stats_ = new shaka.util.Stats();
1318
-
1319
- // Load's request is a little different, so we can't use our normal
1320
- // listeners-to-promise method. It is the only request where we may skip the
1321
- // request, so we need to set the on skip callback to reject with a specific
1322
- // error.
1323
- events.onStart =
1324
- () => shaka.log.info('Starting load of ' + assetUri + '...');
1325
- return new Promise((resolve, reject) => {
1326
- events.onSkip = () => reject(new shaka.util.Error(
1327
- shaka.util.Error.Severity.CRITICAL,
1328
- shaka.util.Error.Category.PLAYER,
1329
- shaka.util.Error.Code.NO_VIDEO_ELEMENT));
1370
+ const startTimeOfLoad = Date.now() / 1000;
1330
1371
 
1331
- events.onEnd = () => {
1332
- resolve();
1333
- // We dispatch the loaded event when the load promise is resolved
1334
- this.dispatchEvent(
1335
- this.makeEvent_(shaka.util.FakeEvent.EventName.Loaded));
1336
- };
1337
- events.onCancel = () => reject(this.createAbortLoadError_());
1338
- events.onError = (e) => reject(e);
1339
- });
1372
+ // Stats are for a single playback/load session. Stats must be initialized
1373
+ // before we allow calls to |updateStateHistory|.
1374
+ this.stats_ = new shaka.util.Stats();
1375
+
1376
+ this.assetUri_ = assetUri;
1377
+
1378
+ if (!mimeType) {
1379
+ await mutexWrapOperation(async () => {
1380
+ mimeType = await this.guessMimeType_();
1381
+ }, 'guessMimeType_');
1382
+ }
1383
+ const shouldUseSrcEquals = this.shouldUseSrcEquals_(assetUri, mimeType);
1384
+ if (shouldUseSrcEquals) {
1385
+ await mutexWrapOperation(async () => {
1386
+ goog.asserts.assert(mimeType, 'We should know the mimeType by now!');
1387
+ await this.initializeSrcEqualsDrmInner_(mimeType);
1388
+ }, 'initializeSrcEqualsDrmInner_');
1389
+ await mutexWrapOperation(async () => {
1390
+ goog.asserts.assert(mimeType, 'We should know the mimeType by now!');
1391
+ await this.srcEqualsInner_(startTimeOfLoad, mimeType);
1392
+ }, 'srcEqualsInner_');
1393
+ } else {
1394
+ if (!this.mediaSourceEngine_) {
1395
+ await mutexWrapOperation(async () => {
1396
+ await this.initializeMediaSourceEngineInner_();
1397
+ }, 'initializeMediaSourceEngineInner_');
1398
+ }
1399
+ await mutexWrapOperation(async () => {
1400
+ await this.parseManifestInner_(mimeType);
1401
+ }, 'parseManifestInner_');
1402
+ await mutexWrapOperation(async () => {
1403
+ await this.initializeDrmInner_();
1404
+ }, 'initializeDrmInner_');
1405
+ await mutexWrapOperation(async () => {
1406
+ await this.loadInner_(startTimeOfLoad);
1407
+ }, 'loadInner_');
1408
+ }
1409
+ this.dispatchEvent(this.makeEvent_(
1410
+ shaka.util.FakeEvent.EventName.Loaded));
1411
+ } catch (error) {
1412
+ if (error.code != shaka.util.Error.Code.LOAD_INTERRUPTED) {
1413
+ await this.unload(/* initializeMediaSource= */ false);
1414
+ }
1415
+ throw error;
1416
+ }
1417
+ }
1418
+
1419
+ /**
1420
+ * Determines the mimeType of the given asset, if we are not told that inside
1421
+ * the loading process.
1422
+ *
1423
+ * @return {!Promise.<?string>} mimeType
1424
+ * @private
1425
+ */
1426
+ async guessMimeType_() {
1427
+ goog.asserts.assert(this.assetUri_, 'should have a uri by now.');
1428
+
1429
+ // If no MIME type is provided, and we can't base it on extension, make a
1430
+ // HEAD request to determine it.
1431
+ goog.asserts.assert(this.networkingEngine_, 'Should have a net engine!');
1432
+ const retryParams = this.config_.manifest.retryParameters;
1433
+ let mimeType = await shaka.net.NetworkingUtils.getMimeType(
1434
+ this.assetUri_, this.networkingEngine_, retryParams);
1435
+ if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) {
1436
+ mimeType = 'application/vnd.apple.mpegurl';
1437
+ }
1438
+ return mimeType;
1340
1439
  }
1341
1440
 
1342
1441
  /**
1343
- * Check if src= should be used to load the asset at |uri|. Assume that media
1344
- * source is the default option, and that src= is for special cases.
1442
+ * Determines if we should use src equals, based on the the mimeType (if
1443
+ * known), the URI, and platform information.
1345
1444
  *
1346
- * @param {shaka.routing.Payload} payload
1445
+ * @param {string} assetUri
1446
+ * @param {?string=} mimeType
1347
1447
  * @return {boolean}
1348
1448
  * |true| if the content should be loaded with src=, |false| if the content
1349
1449
  * should be loaded with MediaSource.
1350
1450
  * @private
1351
1451
  */
1352
- shouldUseSrcEquals_(payload) {
1452
+ shouldUseSrcEquals_(assetUri, mimeType) {
1353
1453
  const Platform = shaka.util.Platform;
1354
1454
  const MimeUtils = shaka.util.MimeUtils;
1355
1455
 
@@ -1359,29 +1459,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1359
1459
  return true;
1360
1460
  }
1361
1461
 
1362
- // The most accurate way to tell the player how to load the content is via
1363
- // MIME type. We can fall back to features of the URI if needed.
1364
- let mimeType = payload.mimeType;
1365
- const uri = payload.uri || '';
1366
-
1367
- // If we don't have a MIME type, try to guess based on the file extension.
1368
- // TODO: Too generic to belong to ManifestParser now. Refactor.
1369
- if (!mimeType) {
1370
- // Try using the uri extension.
1371
- const extension = shaka.media.ManifestParser.getExtension(uri);
1372
- mimeType = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension];
1373
- }
1374
-
1375
- // TODO: The load graph system has a design limitation that requires routing
1376
- // destination to be chosen synchronously. This means we can only make the
1377
- // right choice about src= consistently if we have a well-known file
1378
- // extension or API-provided MIME type. Detection of MIME type from a HEAD
1379
- // request (as is done for manifest types) can't be done yet.
1380
-
1381
1462
  if (mimeType) {
1382
1463
  // If we have a MIME type, check if the browser can play it natively.
1383
1464
  // This will cover both single files and native HLS.
1384
- const mediaElement = payload.mediaElement || Platform.anyMediaElement();
1465
+ const mediaElement = this.video_ || Platform.anyMediaElement();
1385
1466
  const canPlayNatively = mediaElement.canPlayType(mimeType) != '';
1386
1467
 
1387
1468
  // If we can't play natively, then src= isn't an option.
@@ -1390,7 +1471,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1390
1471
  }
1391
1472
 
1392
1473
  const canPlayMediaSource =
1393
- shaka.media.ManifestParser.isSupported(uri, mimeType);
1474
+ shaka.media.ManifestParser.isSupported(mimeType);
1394
1475
 
1395
1476
  // If MediaSource isn't an option, the native option is our only chance.
1396
1477
  if (!canPlayMediaSource) {
@@ -1404,17 +1485,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1404
1485
  // We would prefer MediaSource in some cases, and src= in others. For
1405
1486
  // example, Android has native HLS, but we'd prefer our own MediaSource
1406
1487
  // version there.
1488
+ if (MimeUtils.isHlsType(mimeType)) {
1489
+ // Native HLS can be preferred on any platform via this flag:
1490
+ if (this.config_.streaming.preferNativeHls) {
1491
+ return true;
1492
+ }
1407
1493
 
1408
- // Native HLS can be preferred on any platform via this flag:
1409
- if (MimeUtils.isHlsType(mimeType) &&
1410
- this.config_.streaming.preferNativeHls) {
1411
- return true;
1412
- }
1413
-
1414
- // For Safari, we have an older flag which only applies to this one
1415
- // browser:
1416
- if (Platform.isApple()) {
1417
- return this.config_.streaming.useNativeHlsOnSafari;
1494
+ // For Safari, we have an older flag which only applies to this one
1495
+ // browser:
1496
+ if (Platform.isApple()) {
1497
+ return this.config_.streaming.useNativeHlsOnSafari;
1498
+ }
1418
1499
  }
1419
1500
 
1420
1501
  // In all other cases, we prefer MediaSource.
@@ -1428,298 +1509,40 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1428
1509
  }
1429
1510
 
1430
1511
  /**
1431
- * This should only be called by the load graph when it is time to attach to
1432
- * a media element. The only times this may be called are when we are being
1433
- * asked to re-attach to the current media element, or attach to a new media
1434
- * element while not attached to a media element.
1435
- *
1436
- * This method assumes that it is safe for it to execute, the load-graph is
1437
- * responsible for ensuring all assumptions are true.
1512
+ * Initializes the media source engine.
1438
1513
  *
1439
- * Attaching to a media element is defined as:
1440
- * - Registering error listeners to the media element.
1441
- * - Caching the video element for use outside of the load graph.
1442
- *
1443
- * @param {shaka.routing.Payload} has
1444
- * @param {shaka.routing.Payload} wants
1445
1514
  * @return {!Promise}
1446
1515
  * @private
1447
1516
  */
1448
- onAttach_(has, wants) {
1449
- // If we don't have a media element yet, it means we are entering
1450
- // "attach" from another node.
1451
- //
1452
- // If we have a media element, it should match |wants.mediaElement|
1453
- // because it means we are going from "attach" to "attach".
1454
- //
1455
- // These constraints should be maintained and guaranteed by the routing
1456
- // logic in |getNextStep_|.
1457
- goog.asserts.assert(
1458
- has.mediaElement == null || has.mediaElement == wants.mediaElement,
1459
- 'The routing logic failed. MediaElement requirement failed.');
1460
-
1461
- if (has.mediaElement == null) {
1462
- has.mediaElement = wants.mediaElement;
1463
-
1464
- const onError = (error) => this.onVideoError_(error);
1465
- this.attachEventManager_.listen(has.mediaElement, 'error', onError);
1466
- }
1467
-
1468
- this.video_ = has.mediaElement;
1469
-
1470
- return Promise.resolve();
1471
- }
1472
-
1473
- /**
1474
- * This should only be called by the load graph when it is time to detach from
1475
- * a media element. The only times this may be called are when we are being
1476
- * asked to detach from the current media element, or detach when we are
1477
- * already detached.
1478
- *
1479
- * This method assumes that it is safe for it to execute, the load-graph is
1480
- * responsible for ensuring all assumptions are true.
1481
- *
1482
- * Detaching from a media element is defined as:
1483
- * - Removing error listeners from the media element.
1484
- * - Dropping the cached reference to the video element.
1485
- *
1486
- * @param {shaka.routing.Payload} has
1487
- * @param {shaka.routing.Payload} wants
1488
- * @return {!Promise}
1489
- * @private
1490
- */
1491
- onDetach_(has, wants) {
1492
- // If we were going from "detached" to "detached" we wouldn't have
1493
- // a media element to detach from.
1494
- if (has.mediaElement) {
1495
- this.attachEventManager_.removeAll();
1496
- has.mediaElement = null;
1497
- }
1498
-
1499
- if (this.adManager_) {
1500
- // The ad manager is specific to the video, so detach it too.
1501
- this.adManager_.release();
1502
- }
1503
-
1504
- // Clear our cached copy of the media element.
1505
- this.video_ = null;
1506
-
1507
- return Promise.resolve();
1508
- }
1509
-
1510
- /**
1511
- * This should only be called by the load graph when it is time to unload all
1512
- * currently initialized playback components. Unlike the other load actions,
1513
- * this action is built to be more general. We need to do this because we
1514
- * don't know what state the player will be in before unloading (including
1515
- * after an error occurred in the middle of a transition).
1516
- *
1517
- * This method assumes that any component could be |null| and should be safe
1518
- * to call from any point in the load graph.
1519
- *
1520
- * @param {shaka.routing.Payload} has
1521
- * @param {shaka.routing.Payload} wants
1522
- * @return {!Promise}
1523
- * @private
1524
- */
1525
- async onUnload_(has, wants) {
1526
- // Set the load mode to unload right away so that all the public methods
1527
- // will stop using the internal components. We need to make sure that we
1528
- // are not overriding the destroyed state because we will unload when we are
1529
- // destroying the player.
1530
- if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
1531
- this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
1532
- }
1533
-
1534
- // Run any general cleanup tasks now. This should be here at the top, right
1535
- // after setting loadMode_, so that internal components still exist as they
1536
- // did when the cleanup tasks were registered in the array.
1537
- const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb());
1538
- this.cleanupOnUnload_ = [];
1539
- await Promise.all(cleanupTasks);
1540
-
1541
- // Dispatch the unloading event.
1542
- this.dispatchEvent(
1543
- this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1544
-
1545
- // Remove everything that has to do with loading content from our payload
1546
- // since we are releasing everything that depended on it.
1547
- has.mimeType = null;
1548
- has.startTime = null;
1549
- has.uri = null;
1550
-
1551
- // Release the region timeline, which is created when parsing the manifest.
1552
- if (this.regionTimeline_) {
1553
- this.regionTimeline_.release();
1554
- this.regionTimeline_ = null;
1555
- }
1556
-
1557
- // In most cases we should have a media element. The one exception would
1558
- // be if there was an error and we, by chance, did not have a media element.
1559
- if (has.mediaElement) {
1560
- this.loadEventManager_.removeAll();
1561
- }
1562
-
1563
- // Stop the variant checker timer
1564
- this.checkVariantsTimer_.stop();
1565
-
1566
- // Some observers use some playback components, shutting down the observers
1567
- // first ensures that they don't try to use the playback components
1568
- // mid-destroy.
1569
- if (this.playheadObservers_) {
1570
- this.playheadObservers_.release();
1571
- this.playheadObservers_ = null;
1572
- }
1573
-
1574
- if (this.bufferPoller_) {
1575
- this.bufferPoller_.stop();
1576
- this.bufferPoller_ = null;
1577
- }
1578
-
1579
- // Stop the parser early. Since it is at the start of the pipeline, it
1580
- // should be start early to avoid is pushing new data downstream.
1581
- if (this.parser_) {
1582
- await this.parser_.stop();
1583
- this.parser_ = null;
1584
- this.parserFactory_ = null;
1585
- }
1586
-
1587
- // Abr Manager will tell streaming engine what to do, so we need to stop
1588
- // it before we destroy streaming engine. Unlike with the other components,
1589
- // we do not release the instance, we will reuse it in later loads.
1590
- if (this.abrManager_) {
1591
- await this.abrManager_.stop();
1592
- }
1593
-
1594
- // Streaming engine will push new data to media source engine, so we need
1595
- // to shut it down before destroy media source engine.
1596
- if (this.streamingEngine_) {
1597
- await this.streamingEngine_.destroy();
1598
- this.streamingEngine_ = null;
1599
- }
1600
-
1601
- if (this.playRateController_) {
1602
- this.playRateController_.release();
1603
- this.playRateController_ = null;
1604
- }
1605
-
1606
- // Playhead is used by StreamingEngine, so we can't destroy this until after
1607
- // StreamingEngine has stopped.
1608
- if (this.playhead_) {
1609
- this.playhead_.release();
1610
- this.playhead_ = null;
1611
- }
1612
-
1613
- // Media source engine holds onto the media element, and in order to detach
1614
- // the media keys (with drm engine), we need to break the connection between
1615
- // media source engine and the media element.
1616
- if (this.mediaSourceEngine_) {
1617
- await this.mediaSourceEngine_.destroy();
1618
- this.mediaSourceEngine_ = null;
1619
- }
1620
-
1621
- if (this.adManager_) {
1622
- this.adManager_.onAssetUnload();
1623
- }
1624
-
1625
- // In order to unload a media element, we need to remove the src attribute
1626
- // and then load again. When we destroy media source engine, this will be
1627
- // done for us, but for src=, we need to do it here.
1628
- //
1629
- // DrmEngine requires this to be done before we destroy DrmEngine itself.
1630
- if (has.mediaElement && has.mediaElement.src) {
1631
- // TODO: Investigate this more. Only reproduces on Firefox 69.
1632
- // Introduce a delay before detaching the video source. We are seeing
1633
- // spurious Promise rejections involving an AbortError in our tests
1634
- // otherwise.
1635
- await new Promise(
1636
- (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
1637
-
1638
- has.mediaElement.removeAttribute('src');
1639
- has.mediaElement.load();
1640
- // Remove all track nodes
1641
- while (has.mediaElement.lastChild) {
1642
- has.mediaElement.removeChild(has.mediaElement.firstChild);
1643
- }
1644
- }
1645
-
1646
- if (this.drmEngine_) {
1647
- await this.drmEngine_.destroy();
1648
- this.drmEngine_ = null;
1649
- }
1650
-
1651
- this.assetUri_ = null;
1652
- this.bufferObserver_ = null;
1653
-
1654
- if (this.manifest_) {
1655
- for (const variant of this.manifest_.variants) {
1656
- for (const stream of [variant.audio, variant.video]) {
1657
- if (stream && stream.segmentIndex) {
1658
- stream.segmentIndex.release();
1659
- }
1660
- }
1661
- }
1662
- for (const stream of this.manifest_.textStreams) {
1663
- if (stream.segmentIndex) {
1664
- stream.segmentIndex.release();
1665
- }
1666
- }
1667
- }
1668
-
1669
- this.manifest_ = null;
1670
- this.stats_ = new shaka.util.Stats(); // Replace with a clean stats object.
1671
- this.lastTextFactory_ = null;
1672
-
1673
- // Make sure that the app knows of the new buffering state.
1674
- this.updateBufferState_();
1675
- }
1676
-
1677
- /**
1678
- * This should only be called by the load graph when it is time to initialize
1679
- * media source engine. The only time this may be called is when we are
1680
- * attached to the same media element as in the request.
1681
- *
1682
- * This method assumes that it is safe for it to execute. The load-graph is
1683
- * responsible for ensuring all assumptions are true.
1684
- *
1685
- * @param {shaka.routing.Payload} has
1686
- * @param {shaka.routing.Payload} wants
1687
- *
1688
- * @return {!Promise}
1689
- * @private
1690
- */
1691
- async onInitializeMediaSourceEngine_(has, wants) {
1517
+ async initializeMediaSourceEngineInner_() {
1692
1518
  goog.asserts.assert(
1693
1519
  shaka.util.Platform.supportsMediaSource(),
1694
- 'We should not be initializing media source on a platform that does ' +
1695
- 'not support media source.');
1520
+ 'We should not be initializing media source on a platform that ' +
1521
+ 'does not support media source.');
1696
1522
  goog.asserts.assert(
1697
- has.mediaElement,
1523
+ this.video_,
1698
1524
  'We should have a media element when initializing media source.');
1699
- goog.asserts.assert(
1700
- has.mediaElement == wants.mediaElement,
1701
- '|has| and |wants| should have the same media element when ' +
1702
- 'initializing media source.');
1703
-
1704
1525
  goog.asserts.assert(
1705
1526
  this.mediaSourceEngine_ == null,
1706
1527
  'We should not have a media source engine yet.');
1707
1528
 
1529
+ this.makeStateChangeEvent_('media-source');
1530
+
1708
1531
  // When changing text visibility we need to update both the text displayer
1709
- // and streaming engine because we don't always stream text. To ensure that
1710
- // text displayer and streaming engine are always in sync, wait until they
1711
- // are both initialized before setting the initial value.
1532
+ // and streaming engine because we don't always stream text. To ensure
1533
+ // that the text displayer and streaming engine are always in sync, wait
1534
+ // until they are both initialized before setting the initial value.
1712
1535
  const textDisplayerFactory = this.config_.textDisplayFactory;
1713
1536
  const textDisplayer = textDisplayerFactory();
1714
1537
  this.lastTextFactory_ = textDisplayerFactory;
1715
1538
 
1716
1539
  const mediaSourceEngine = this.createMediaSourceEngine(
1717
- has.mediaElement,
1540
+ this.video_,
1718
1541
  textDisplayer,
1719
1542
  (metadata, offset, endTime) => {
1720
1543
  this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
1721
1544
  },
1722
- this.lcevcDil_);
1545
+ this.lcevcDec_);
1723
1546
  mediaSourceEngine.configure(this.config_.mediaSource);
1724
1547
  const {segmentRelativeVttTiming} = this.config_.manifest;
1725
1548
  mediaSourceEngine.setSegmentRelativeVttTiming(segmentRelativeVttTiming);
@@ -1733,114 +1556,51 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1733
1556
  }
1734
1557
 
1735
1558
  /**
1736
- * Create the parser for the asset located at |wants.uri|. This should only be
1737
- * called as part of the load graph.
1738
- *
1739
- * This method assumes that it is safe for it to execute, the load-graph is
1740
- * responsible for ensuring all assumptions are true.
1559
+ * Pick an initialize a manifest parser, then have it download and parse the
1560
+ * manifest.
1741
1561
  *
1742
- * @param {shaka.routing.Payload} has
1743
- * @param {shaka.routing.Payload} wants
1562
+ * @param {?string=} mimeType
1744
1563
  * @return {!Promise}
1745
1564
  * @private
1746
1565
  */
1747
- async onInitializeParser_(has, wants) {
1566
+ async parseManifestInner_(mimeType) {
1748
1567
  goog.asserts.assert(
1749
- has.mediaElement,
1750
- 'We should have a media element when initializing the parser.');
1568
+ this.assetUri_,
1569
+ 'should have a uri when making the parser.');
1751
1570
  goog.asserts.assert(
1752
- has.mediaElement == wants.mediaElement,
1753
- '|has| and |wants| should have the same media element when ' +
1754
- 'initializing the parser.');
1755
-
1571
+ this.video_,
1572
+ 'We should have a media element when initializing the parser.');
1756
1573
  goog.asserts.assert(
1757
1574
  this.networkingEngine_,
1758
1575
  'Need networking engine when initializing the parser.');
1576
+ goog.asserts.assert(
1577
+ this.cmcdManager_,
1578
+ 'Need CMCD manager to populate manifest request data.');
1759
1579
  goog.asserts.assert(
1760
1580
  this.config_,
1761
1581
  'Need player config when initializing the parser.');
1762
1582
 
1763
- // We are going to "lock-in" the mime type and uri since they are
1764
- // what we are going to use to create our parser and parse the manifest.
1765
- has.mimeType = wants.mimeType;
1766
- has.uri = wants.uri;
1767
-
1768
- goog.asserts.assert(
1769
- has.uri,
1770
- 'We should have an asset uri when initializing the parsing.');
1771
-
1772
- // Store references to things we asserted so that we don't need to reassert
1773
- // them again later.
1774
- const assetUri = has.uri;
1583
+ // Store references to things we asserted so that we don't need to
1584
+ // reassert them again later.
1775
1585
  const networkingEngine = this.networkingEngine_;
1776
1586
 
1777
- // Save the uri so that it can be used outside of the load-graph.
1778
- this.assetUri_ = assetUri;
1587
+ this.makeStateChangeEvent_('manifest-parser');
1779
1588
 
1780
1589
  // Create the parser that we will use to parse the manifest.
1781
- this.parserFactory_ = await shaka.media.ManifestParser.getFactory(
1782
- assetUri,
1783
- networkingEngine,
1784
- this.config_.manifest.retryParameters,
1785
- has.mimeType);
1590
+ this.parserFactory_ = shaka.media.ManifestParser.getFactory(
1591
+ this.assetUri_,
1592
+ mimeType || null);
1786
1593
  goog.asserts.assert(this.parserFactory_, 'Must have manifest parser');
1787
1594
  this.parser_ = this.parserFactory_();
1788
1595
 
1789
1596
  const manifestConfig =
1790
1597
  shaka.util.ObjectUtils.cloneObject(this.config_.manifest);
1791
1598
  // Don't read video segments if the player is attached to an audio element
1792
- if (wants.mediaElement && wants.mediaElement.nodeName === 'AUDIO') {
1599
+ if (this.video_ && this.video_.nodeName === 'AUDIO') {
1793
1600
  manifestConfig.disableVideo = true;
1794
1601
  }
1795
1602
 
1796
1603
  this.parser_.configure(manifestConfig);
1797
- }
1798
-
1799
- /**
1800
- * Parse the manifest at |has.uri| using the parser that should have already
1801
- * been created. This should only be called as part of the load graph.
1802
- *
1803
- * This method assumes that it is safe for it to execute, the load-graph is
1804
- * responsible for ensuring all assumptions are true.
1805
- *
1806
- * @param {shaka.routing.Payload} has
1807
- * @param {shaka.routing.Payload} wants
1808
- * @return {!shaka.util.AbortableOperation}
1809
- * @private
1810
- */
1811
- onParseManifest_(has, wants) {
1812
- goog.asserts.assert(
1813
- has.mimeType == wants.mimeType,
1814
- '|has| and |wants| should have the same mime type when parsing.');
1815
- goog.asserts.assert(
1816
- has.uri == wants.uri,
1817
- '|has| and |wants| should have the same uri when parsing.');
1818
-
1819
- goog.asserts.assert(
1820
- has.uri,
1821
- '|has| should have a valid uri when parsing.');
1822
- goog.asserts.assert(
1823
- has.uri == this.assetUri_,
1824
- '|has.uri| should match the cached asset uri.');
1825
-
1826
- goog.asserts.assert(
1827
- this.networkingEngine_,
1828
- 'Need networking engine to parse manifest.');
1829
- goog.asserts.assert(
1830
- this.cmcdManager_,
1831
- 'Need CMCD manager to populate manifest request data.');
1832
- goog.asserts.assert(
1833
- this.config_,
1834
- 'Need player config to parse manifest.');
1835
-
1836
- goog.asserts.assert(
1837
- this.parser_,
1838
- '|this.parser_| should have been set in an earlier step.');
1839
-
1840
- // Store references to things we asserted so that we don't need to reassert
1841
- // them again later.
1842
- const assetUri = has.uri;
1843
- const networkingEngine = this.networkingEngine_;
1844
1604
 
1845
1605
  // This will be needed by the parser once it starts parsing, so we will
1846
1606
  // initialize it now even through it appears a little out-of-place.
@@ -1881,7 +1641,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1881
1641
  // Called when the parser finds a timeline region. This can be called
1882
1642
  // before we start playback or during playback (live/in-progress
1883
1643
  // manifest).
1884
- onTimelineRegionAdded: (region) => this.regionTimeline_.addRegion(region),
1644
+ onTimelineRegionAdded: (region) => {
1645
+ this.regionTimeline_.addRegion(region);
1646
+ },
1885
1647
 
1886
1648
  onEvent: (event) => this.dispatchEvent(event),
1887
1649
  onError: (error) => this.onError_(error),
@@ -1904,61 +1666,54 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1904
1666
  this.processDrmInfos_(currentDrmInfo.keySystem, stream);
1905
1667
  }
1906
1668
  },
1669
+ onManifestUpdated: () => {
1670
+ const eventName = shaka.util.FakeEvent.EventName.ManifestUpdated;
1671
+ const data = (new Map()).set('isLive', this.isLive());
1672
+ this.dispatchEvent(this.makeEvent_(eventName, data));
1673
+ if (this.adManager_) {
1674
+ this.adManager_.onManifestUpdated(this.isLive());
1675
+ }
1676
+ },
1677
+ getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
1907
1678
  };
1908
1679
 
1909
1680
  const startTime = Date.now() / 1000;
1910
1681
 
1911
- return new shaka.util.AbortableOperation(/* promise= */ (async () => {
1912
- this.manifest_ = await this.parser_.start(assetUri, playerInterface);
1682
+ this.makeStateChangeEvent_('manifest');
1913
1683
 
1914
- // This event is fired after the manifest is parsed, but before any
1915
- // filtering takes place.
1916
- const event =
1917
- this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
1918
- this.dispatchEvent(event);
1684
+ this.manifest_ = await this.parser_.start(
1685
+ this.assetUri_, playerInterface);
1919
1686
 
1920
- // We require all manifests to have at least one variant.
1921
- if (this.manifest_.variants.length == 0) {
1922
- throw new shaka.util.Error(
1923
- shaka.util.Error.Severity.CRITICAL,
1924
- shaka.util.Error.Category.MANIFEST,
1925
- shaka.util.Error.Code.NO_VARIANTS);
1926
- }
1687
+ // This event is fired after the manifest is parsed, but before any
1688
+ // filtering takes place.
1689
+ const event =
1690
+ this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
1691
+ this.dispatchEvent(event);
1927
1692
 
1928
- // Make sure that all variants are either: audio-only, video-only, or
1929
- // audio-video.
1930
- shaka.Player.filterForAVVariants_(this.manifest_);
1693
+ // We require all manifests to have at least one variant.
1694
+ if (this.manifest_.variants.length == 0) {
1695
+ throw new shaka.util.Error(
1696
+ shaka.util.Error.Severity.CRITICAL,
1697
+ shaka.util.Error.Category.MANIFEST,
1698
+ shaka.util.Error.Code.NO_VARIANTS);
1699
+ }
1931
1700
 
1932
- const now = Date.now() / 1000;
1933
- const delta = now - startTime;
1934
- this.stats_.setManifestTime(delta);
1935
- })(), /* onAbort= */ () => {
1936
- shaka.log.info('Aborting parser step...');
1937
- return this.parser_.stop();
1938
- });
1701
+ // Make sure that all variants are either: audio-only, video-only, or
1702
+ // audio-video.
1703
+ shaka.Player.filterForAVVariants_(this.manifest_);
1704
+
1705
+ const now = Date.now() / 1000;
1706
+ const delta = now - startTime;
1707
+ this.stats_.setManifestTime(delta);
1939
1708
  }
1940
1709
 
1941
1710
  /**
1942
- * This should only be called by the load graph when it is time to initialize
1943
- * drmEngine. The only time this may be called is when we are attached a
1944
- * media element and have parsed a manifest.
1945
- *
1946
- * The load-graph is responsible for ensuring all assumptions made by this
1947
- * method are valid before executing it.
1711
+ * Initializes the DRM engine.
1948
1712
  *
1949
- * @param {shaka.routing.Payload} has
1950
- * @param {shaka.routing.Payload} wants
1951
1713
  * @return {!Promise}
1952
1714
  * @private
1953
1715
  */
1954
- async onInitializeDrm_(has, wants) {
1955
- goog.asserts.assert(
1956
- has.mimeType == wants.mimeType,
1957
- 'The load graph should have ensured the mime types matched.');
1958
- goog.asserts.assert(
1959
- has.uri == wants.uri,
1960
- 'The load graph should have ensured the uris matched');
1961
-
1716
+ async initializeDrmInner_() {
1962
1717
  goog.asserts.assert(
1963
1718
  this.networkingEngine_,
1964
1719
  '|onInitializeDrm_| should never be called after |destroy|');
@@ -1969,9 +1724,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1969
1724
  this.manifest_,
1970
1725
  '|this.manifest_| should have been set in an earlier step.');
1971
1726
  goog.asserts.assert(
1972
- has.mediaElement,
1727
+ this.video_,
1973
1728
  'We should have a media element when initializing the DRM Engine.');
1974
1729
 
1730
+ this.makeStateChangeEvent_('drm-engine');
1731
+
1975
1732
  const startTime = Date.now() / 1000;
1976
1733
  let firstEvent = true;
1977
1734
 
@@ -1994,14 +1751,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1994
1751
  const now = Date.now() / 1000;
1995
1752
  const delta = now - startTime;
1996
1753
  this.stats_.setDrmTime(delta);
1997
- // LCEVC data by itself is not encrypted in DRM protected streams and
1998
- // can therefore be accessed and decoded as normal. However, the LCEVC
1999
- // decoder needs access to the VideoElement output in order to apply
2000
- // the enhancement. In DRM contexts where the browser CDM restricts
2001
- // access from our decoder, the enhancement cannot be applied and
2002
- // therefore the LCEVC output canvas is hidden accordingly.
2003
- if (this.lcevcDil_) {
2004
- this.lcevcDil_.hideCanvas();
1754
+ // LCEVC data by itself is not encrypted in DRM protected streams
1755
+ // and can therefore be accessed and decoded as normal. However,
1756
+ // the LCEVC decoder needs access to the VideoElement output in
1757
+ // order to apply the enhancement. In DRM contexts where the
1758
+ // browser CDM restricts access from our decoder, the enhancement
1759
+ // cannot be applied and therefore the LCEVC output canvas is
1760
+ // hidden accordingly.
1761
+ if (this.lcevcDec_) {
1762
+ this.lcevcDec_.hideCanvas();
2005
1763
  }
2006
1764
  }
2007
1765
  },
@@ -2013,66 +1771,31 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2013
1771
  this.manifest_.variants,
2014
1772
  this.manifest_.offlineSessionIds);
2015
1773
 
2016
- await this.drmEngine_.attach(has.mediaElement);
1774
+ await this.drmEngine_.attach(this.video_);
2017
1775
 
2018
- // Now that we have drm information, filter the manifest (again) so that we
2019
- // can ensure we only use variants with the selected key system.
1776
+ // Now that we have drm information, filter the manifest (again) so that
1777
+ // we can ensure we only use variants with the selected key system.
2020
1778
  await this.filterManifest_(this.manifest_);
2021
1779
  }
2022
1780
 
2023
1781
  /**
2024
- * This should only be called by the load graph when it is time to load all
2025
- * playback components needed for playback. The only times this may be called
2026
- * is when we are attached to the same media element as in the request.
2027
- *
2028
- * This method assumes that it is safe for it to execute, the load-graph is
2029
- * responsible for ensuring all assumptions are true.
2030
- *
2031
- * Loading is defined as:
2032
- * - Attaching all playback-related listeners to the media element
2033
- * - Initializing playback and observers
2034
- * - Initializing ABR Manager
2035
- * - Initializing Streaming Engine
2036
- * - Starting playback at |wants.startTime|
1782
+ * Starts loading the content described by the parsed manifest.
2037
1783
  *
2038
- * @param {shaka.routing.Payload} has
2039
- * @param {shaka.routing.Payload} wants
1784
+ * @param {number} startTimeOfLoad
1785
+ * @return {!Promise}
2040
1786
  * @private
2041
1787
  */
2042
- async onLoad_(has, wants) {
1788
+ async loadInner_(startTimeOfLoad) {
2043
1789
  goog.asserts.assert(
2044
- has.mimeType == wants.mimeType,
2045
- '|has| and |wants| should have the same mime type when loading.');
2046
- goog.asserts.assert(
2047
- has.uri == wants.uri,
2048
- '|has| and |wants| should have the same uri when loading.');
2049
-
1790
+ this.video_, 'We should have a media element by now.');
2050
1791
  goog.asserts.assert(
2051
- has.mediaElement,
2052
- 'We should have a media element when loading.');
1792
+ this.manifest_, 'The manifest should already be parsed.');
2053
1793
  goog.asserts.assert(
2054
- !isNaN(wants.startTimeOfLoad),
2055
- '|wants| should tell us when the load was originally requested');
1794
+ this.assetUri_, 'We should have an asset uri by now.');
2056
1795
 
2057
- // Since we are about to start playback, we will lock in the start time as
2058
- // something we are now depending on.
2059
- has.startTime = wants.startTime;
2060
-
2061
- // If updateStartTime() has been called since load() was invoked use the
2062
- // requested startTime
2063
- if (this.updatedStartTime_ != null) {
2064
- has.startTime = this.updatedStartTime_;
2065
- this.updatedStartTime_ = null;
2066
- }
2067
-
2068
- // Store a reference to values in |has| after asserting so that closure will
2069
- // know that they will still be non-null between calls to await.
2070
- const mediaElement = has.mediaElement;
2071
- const assetUri = has.uri;
2072
-
2073
- // Save the uri so that it can be used outside of the load-graph.
2074
- this.assetUri_ = assetUri;
1796
+ this.makeStateChangeEvent_('load');
2075
1797
 
1798
+ const mediaElement = this.video_;
2076
1799
  this.playRateController_ = new shaka.media.PlayRateController({
2077
1800
  getRate: () => mediaElement.playbackRate,
2078
1801
  getDefaultRate: () => mediaElement.defaultPlaybackRate,
@@ -2082,13 +1805,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2082
1805
 
2083
1806
  const updateStateHistory = () => this.updateStateHistory_();
2084
1807
  const onRateChange = () => this.onRateChange_();
2085
- this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory);
1808
+ this.loadEventManager_.listen(
1809
+ mediaElement, 'playing', updateStateHistory);
2086
1810
  this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
2087
1811
  this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
2088
1812
  this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
2089
1813
 
2090
- // Check status of the LCEVC DIL Object and reset or
2091
- // create or close based on config
1814
+ // Check the status of the LCEVC Dec Object. Reset, create, or close
1815
+ // depending on the config.
2092
1816
  this.setupLcevc_(this.config_);
2093
1817
 
2094
1818
  const abrFactory = this.config_.abrFactory;
@@ -2112,7 +1836,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2112
1836
  this.config_.preferredVariantRole,
2113
1837
  this.config_.preferredAudioChannelCount,
2114
1838
  this.config_.preferredVideoHdrLevel,
2115
- this.config_.preferredAudioLabel);
1839
+ this.config_.preferredVideoLayout,
1840
+ this.config_.preferredAudioLabel,
1841
+ this.config_.mediaSource.codecSwitchingStrategy,
1842
+ this.config_.manifest.dash.enableAudioGroups);
2116
1843
 
2117
1844
  this.currentTextLanguage_ = this.config_.preferredTextLanguage;
2118
1845
  this.currentTextRole_ = this.config_.preferredTextRole;
@@ -2133,7 +1860,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2133
1860
  this.manifest_,
2134
1861
  this.config_.preferredVideoCodecs,
2135
1862
  this.config_.preferredAudioCodecs,
2136
- this.config_.preferredAudioChannelCount,
2137
1863
  this.config_.preferredDecodingAttributes);
2138
1864
 
2139
1865
  this.streamingEngine_ = this.createStreamingEngine();
@@ -2175,7 +1901,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2175
1901
  let initialVariant = null;
2176
1902
  const activeVariant = this.streamingEngine_.getCurrentVariant();
2177
1903
  if (!activeVariant) {
2178
- initialVariant = this.chooseVariant_();
1904
+ initialVariant = this.chooseVariant_(/* initialSelection= */ true);
2179
1905
  goog.asserts.assert(initialVariant, 'Must choose an initial variant!');
2180
1906
  }
2181
1907
 
@@ -2191,20 +1917,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2191
1917
  await Promise.all(createSegmentIndexPromises);
2192
1918
  }
2193
1919
 
1920
+ if (this.parser_ && this.parser_.onInitialVariantChosen) {
1921
+ this.parser_.onInitialVariantChosen(toLazyLoad);
1922
+ }
1923
+
2194
1924
  shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
2195
1925
  this.config_.playRangeStart,
2196
1926
  this.config_.playRangeEnd);
2197
1927
 
2198
- this.playhead_ = this.createPlayhead(has.startTime);
1928
+ this.playhead_ = this.createPlayhead(this.startTime_);
2199
1929
  this.playheadObservers_ =
2200
- this.createPlayheadObserversForMSE_(has.startTimeOfLoad);
1930
+ this.createPlayheadObserversForMSE_(startTimeOfLoad);
2201
1931
 
2202
- // We need to start the buffer management code near the end because it will
2203
- // set the initial buffering state and that depends on other components
2204
- // being initialized.
1932
+ // We need to start the buffer management code near the end because it
1933
+ // will set the initial buffering state and that depends on other
1934
+ // components being initialized.
2205
1935
  const rebufferThreshold = Math.max(
2206
1936
  this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal);
2207
- this.startBufferManagement_(rebufferThreshold);
1937
+ this.startBufferManagement_(mediaElement, rebufferThreshold);
2208
1938
 
2209
1939
  // Now we can switch to the initial variant.
2210
1940
  if (!activeVariant) {
@@ -2214,14 +1944,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2214
1944
  this.switchVariant_(initialVariant, /* fromAdaptation= */ true,
2215
1945
  /* clearBuffer= */ false, /* safeMargin= */ 0);
2216
1946
 
2217
- // Now that we have initial streams, we may adjust the start time to align
2218
- // to a segment boundary.
1947
+ // Now that we have initial streams, we may adjust the start time to
1948
+ // align to a segment boundary.
2219
1949
  if (this.config_.streaming.startAtSegmentBoundary) {
2220
- const startTime = this.playhead_.getTime();
2221
- const adjustedTime =
2222
- await this.adjustStartTime_(initialVariant, startTime);
2223
-
2224
- this.playhead_.setStartTime(adjustedTime);
1950
+ this.playhead_.setStartTime(await this.adjustStartTime_(
1951
+ initialVariant, this.playhead_.getTime()));
2225
1952
  }
2226
1953
  }
2227
1954
 
@@ -2244,7 +1971,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2244
1971
  this.setInitialTextState_(initialVariant, initialTextStream);
2245
1972
  }
2246
1973
 
2247
- // Don't initialize with a text stream unless we should be streaming text.
1974
+ // Don't initialize with a text stream unless we should be streaming
1975
+ // text.
2248
1976
  if (initialTextStream && this.shouldStreamText_()) {
2249
1977
  this.streamingEngine_.switchTextStream(initialTextStream);
2250
1978
  }
@@ -2260,9 +1988,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2260
1988
  this.onAbrStatusChanged_();
2261
1989
  }
2262
1990
 
2263
- // Re-filter the manifest after streams have been chosen.
2264
- this.filterManifestByCurrentVariant_();
2265
- // Dispatch a 'trackschanged' event now that all initial filtering is done.
1991
+ // Dispatch a 'trackschanged' event now that all initial filtering is
1992
+ // done.
2266
1993
  this.onTracksChanged_();
2267
1994
 
2268
1995
  // Now that we've filtered out variants that aren't compatible with the
@@ -2276,14 +2003,25 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2276
2003
 
2277
2004
  const hasPrimary = this.manifest_.variants.some((v) => v.primary);
2278
2005
  if (!this.config_.preferredAudioLanguage && !hasPrimary) {
2279
- shaka.log.warning('No preferred audio language set. We have chosen an ' +
2280
- 'arbitrary language initially');
2006
+ shaka.log.warning('No preferred audio language set. ' +
2007
+ 'We have chosen an arbitrary language initially');
2281
2008
  }
2282
2009
 
2283
- if (this.isLive() && (this.config_.streaming.liveSync ||
2010
+ const isLive = this.isLive();
2011
+
2012
+ if (isLive && (this.config_.streaming.liveSync ||
2284
2013
  this.manifest_.serviceDescription)) {
2285
2014
  const onTimeUpdate = () => this.onTimeUpdate_();
2286
2015
  this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
2016
+ } else if (!isLive) {
2017
+ const onVideoProgress = () => this.onVideoProgress_();
2018
+ this.loadEventManager_.listen(
2019
+ mediaElement, 'timeupdate', onVideoProgress);
2020
+ this.onVideoProgress_();
2021
+ }
2022
+
2023
+ if (this.adManager_) {
2024
+ this.adManager_.onManifestUpdated(isLive);
2287
2025
  }
2288
2026
 
2289
2027
  this.fullyLoaded_ = true;
@@ -2291,24 +2029,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2291
2029
  // Wait for the 'loadedmetadata' event to measure load() latency.
2292
2030
  this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2293
2031
  const now = Date.now() / 1000;
2294
- const delta = now - wants.startTimeOfLoad;
2032
+ const delta = now - startTimeOfLoad;
2295
2033
  this.stats_.setLoadLatency(delta);
2296
2034
  });
2297
2035
  }
2298
2036
 
2299
2037
  /**
2300
- * This should only be called by the load graph when it is time to initialize
2301
- * drmEngine for src= playbacks.
2302
- *
2303
- * The load-graph is responsible for ensuring all assumptions made by this
2304
- * method are valid before executing it.
2038
+ * Initializes the DRM engine for use by src equals.
2305
2039
  *
2306
- * @param {shaka.routing.Payload} has
2307
- * @param {shaka.routing.Payload} wants
2040
+ * @param {string} mimeType
2308
2041
  * @return {!Promise}
2309
2042
  * @private
2310
2043
  */
2311
- async onInitializeSrcEqualsDrm_(has, wants) {
2044
+ async initializeSrcEqualsDrmInner_(mimeType) {
2312
2045
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
2313
2046
 
2314
2047
  goog.asserts.assert(
@@ -2346,20 +2079,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2346
2079
 
2347
2080
  this.drmEngine_.configure(this.config_.drm);
2348
2081
 
2349
- const uri = wants.uri || '';
2350
- const extension = shaka.media.ManifestParser.getExtension(uri);
2351
- let mimeType = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension];
2352
- if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) {
2353
- mimeType = 'application/vnd.apple.mpegurl';
2354
- }
2355
- if (!mimeType) {
2356
- mimeType = 'video/mp4';
2357
- }
2358
-
2359
2082
  // TODO: Instead of feeding DrmEngine with Variants, we should refactor
2360
2083
  // DrmEngine so that it takes a minimal config derived from Variants. In
2361
2084
  // cases like this one or in removal of stored content, the details are
2362
- // largely unimportant. We should have a saner way to initialize DrmEngine.
2085
+ // largely unimportant. We should have a saner way to initialize
2086
+ // DrmEngine.
2363
2087
  // That would also insulate DrmEngine from manifest changes in the future.
2364
2088
  // For now, that is time-consuming and this synthetic Variant is easy, so
2365
2089
  // I'm putting it off. Since this is only expected to be used for native
@@ -2374,12 +2098,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2374
2098
  video: {
2375
2099
  id: 0,
2376
2100
  originalId: null,
2101
+ groupId: null,
2377
2102
  createSegmentIndex: () => Promise.resolve(),
2378
2103
  segmentIndex: null,
2379
- mimeType: wants.mimeType ?
2380
- shaka.util.MimeUtils.getBasicType(wants.mimeType) : mimeType,
2381
- codecs: wants.mimeType ?
2382
- shaka.util.MimeUtils.getCodecs(wants.mimeType) : '',
2104
+ mimeType: mimeType ? shaka.util.MimeUtils.getBasicType(mimeType) : '',
2105
+ codecs: mimeType ? shaka.util.MimeUtils.getCodecs(mimeType) : '',
2383
2106
  encrypted: true,
2384
2107
  drmInfos: [], // Filled in by DrmEngine config.
2385
2108
  keyIds: new Set(),
@@ -2398,6 +2121,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2398
2121
  closedCaptions: null,
2399
2122
  accessibilityPurpose: null,
2400
2123
  external: false,
2124
+ fastSwitching: false,
2401
2125
  },
2402
2126
  bandwidth: 100,
2403
2127
  allowedByApplication: true,
@@ -2408,46 +2132,28 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2408
2132
  this.drmEngine_.setSrcEquals(/* srcEquals= */ true);
2409
2133
  await this.drmEngine_.initForPlayback(
2410
2134
  [variant], /* offlineSessionIds= */ []);
2411
- await this.drmEngine_.attach(has.mediaElement);
2135
+ await this.drmEngine_.attach(this.video_);
2412
2136
  }
2413
2137
 
2414
2138
  /**
2415
- * This should only be called by the load graph when it is time to set-up the
2416
- * media element to play content using src=. The only times this may be called
2417
- * is when we are attached to the same media element as in the request.
2418
- *
2419
- * This method assumes that it is safe for it to execute, the load-graph is
2420
- * responsible for ensuring all assumptions are true.
2139
+ * Passes the asset URI along to the media element, so it can be played src
2140
+ * equals style.
2421
2141
  *
2422
- * @param {shaka.routing.Payload} has
2423
- * @param {shaka.routing.Payload} wants
2424
- * @return {!shaka.util.AbortableOperation}
2142
+ * @param {number} startTimeOfLoad
2143
+ * @param {string} mimeType
2144
+ * @return {!Promise}
2425
2145
  *
2426
2146
  * @private
2427
2147
  */
2428
- onSrcEquals_(has, wants) {
2429
- goog.asserts.assert(
2430
- has.mediaElement,
2431
- 'We should have a media element when loading.');
2432
- goog.asserts.assert(
2433
- wants.uri,
2434
- '|has| should have a valid uri when loading.');
2148
+ async srcEqualsInner_(startTimeOfLoad, mimeType) {
2149
+ this.makeStateChangeEvent_('src-equals');
2150
+
2435
2151
  goog.asserts.assert(
2436
- !isNaN(wants.startTimeOfLoad),
2437
- '|wants| should tell us when the load was originally requested');
2152
+ this.video_, 'We should have a media element when loading.');
2438
2153
  goog.asserts.assert(
2439
- this.video_ == has.mediaElement,
2440
- 'The video element should match our media element');
2441
-
2442
- // Lock-in the values that we are using so that the routing logic knows what
2443
- // we have.
2444
- has.uri = wants.uri;
2445
- has.startTime = wants.startTime;
2446
-
2447
- // Save the uri so that it can be used outside of the load-graph.
2448
- this.assetUri_ = has.uri;
2154
+ this.assetUri_, 'We should have a valid uri when loading.');
2449
2155
 
2450
- const mediaElement = has.mediaElement;
2156
+ const mediaElement = this.video_;
2451
2157
 
2452
2158
  this.playhead_ = new shaka.media.SrcEqualsPlayhead(mediaElement);
2453
2159
 
@@ -2458,8 +2164,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2458
2164
  unloaded = true;
2459
2165
  });
2460
2166
 
2461
- if (has.startTime != null) {
2462
- this.playhead_.setStartTime(has.startTime);
2167
+ if (this.startTime_ != null) {
2168
+ this.playhead_.setStartTime(this.startTime_);
2463
2169
  }
2464
2170
 
2465
2171
  this.playRateController_ = new shaka.media.PlayRateController({
@@ -2469,16 +2175,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2469
2175
  movePlayhead: (delta) => { mediaElement.currentTime += delta; },
2470
2176
  });
2471
2177
 
2472
- // We need to start the buffer management code near the end because it will
2473
- // set the initial buffering state and that depends on other components
2474
- // being initialized.
2178
+ // We need to start the buffer management code near the end because it
2179
+ // will set the initial buffering state and that depends on other
2180
+ // components being initialized.
2475
2181
  const rebufferThreshold = this.config_.streaming.rebufferingGoal;
2476
- this.startBufferManagement_(rebufferThreshold);
2182
+ this.startBufferManagement_(mediaElement, rebufferThreshold);
2477
2183
 
2478
2184
  // Add all media element listeners.
2479
2185
  const updateStateHistory = () => this.updateStateHistory_();
2480
2186
  const onRateChange = () => this.onRateChange_();
2481
- this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory);
2187
+ this.loadEventManager_.listen(
2188
+ mediaElement, 'playing', updateStateHistory);
2482
2189
  this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
2483
2190
  this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
2484
2191
  this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
@@ -2488,24 +2195,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2488
2195
  // automatically.
2489
2196
  // See https://github.com/shaka-project/shaka-player/issues/2483
2490
2197
  if (mediaElement.preload != 'none') {
2491
- this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2492
- const now = Date.now() / 1000;
2493
- const delta = now - wants.startTimeOfLoad;
2494
- this.stats_.setLoadLatency(delta);
2495
- });
2198
+ this.loadEventManager_.listenOnce(
2199
+ mediaElement, 'loadedmetadata', () => {
2200
+ const now = Date.now() / 1000;
2201
+ const delta = now - startTimeOfLoad;
2202
+ this.stats_.setLoadLatency(delta);
2203
+ });
2496
2204
  }
2497
2205
 
2498
2206
  // The audio tracks are only available on Safari at the moment, but this
2499
2207
  // drives the tracks API for Safari's native HLS. So when they change,
2500
2208
  // fire the corresponding Shaka Player event.
2501
2209
  if (mediaElement.audioTracks) {
2502
- this.loadEventManager_.listen(
2503
- mediaElement.audioTracks, 'addtrack', () => this.onTracksChanged_());
2504
- this.loadEventManager_.listen(
2505
- mediaElement.audioTracks, 'removetrack',
2210
+ this.loadEventManager_.listen(mediaElement.audioTracks, 'addtrack',
2211
+ () => this.onTracksChanged_());
2212
+ this.loadEventManager_.listen(mediaElement.audioTracks, 'removetrack',
2213
+ () => this.onTracksChanged_());
2214
+ this.loadEventManager_.listen(mediaElement.audioTracks, 'change',
2506
2215
  () => this.onTracksChanged_());
2507
- this.loadEventManager_.listen(
2508
- mediaElement.audioTracks, 'change', () => this.onTracksChanged_());
2509
2216
  }
2510
2217
 
2511
2218
  if (mediaElement.textTracks) {
@@ -2541,13 +2248,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2541
2248
  () => this.onTracksChanged_());
2542
2249
  }
2543
2250
 
2544
- const extension = shaka.media.ManifestParser.getExtension(has.uri);
2545
- const mimeType =
2546
- shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension];
2547
-
2548
2251
  // By setting |src| we are done "loading" with src=. We don't need to set
2549
2252
  // the current time because |playhead| will do that for us.
2550
- mediaElement.src = this.cmcdManager_.appendSrcData(has.uri, mimeType);
2253
+ mediaElement.src = this.cmcdManager_.appendSrcData(
2254
+ this.assetUri_, mimeType);
2551
2255
 
2552
2256
  // Tizen 3 / WebOS won't load anything unless you call load() explicitly,
2553
2257
  // no matter the value of the preload attribute. This is harmful on some
@@ -2561,15 +2265,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2561
2265
  // initialized.
2562
2266
  this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS;
2563
2267
 
2564
- // The event doesn't mean as much for src= playback, since we don't control
2565
- // streaming. But we should fire it in this path anyway since some
2566
- // applications may be expecting it as a life-cycle event.
2268
+ // The event doesn't mean as much for src= playback, since we don't
2269
+ // control streaming. But we should fire it in this path anyway since
2270
+ // some applications may be expecting it as a life-cycle event.
2567
2271
  this.dispatchEvent(
2568
2272
  this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
2569
2273
 
2570
2274
  // The "load" Promise is resolved when we have loaded the metadata. If we
2571
- // wait for the full data, that won't happen on Safari until the play button
2572
- // is hit.
2275
+ // wait for the full data, that won't happen on Safari until the play
2276
+ // button is hit.
2573
2277
  const fullyLoaded = new shaka.util.PublicPromise();
2574
2278
  shaka.util.MediaReadyState.waitForReadyState(mediaElement,
2575
2279
  HTMLMediaElement.HAVE_METADATA,
@@ -2579,7 +2283,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2579
2283
  fullyLoaded.resolve();
2580
2284
  });
2581
2285
 
2582
- // We can't switch to preferred languages, though, until the data is loaded.
2286
+ // We can't switch to preferred languages, though, until the data is
2287
+ // loaded.
2583
2288
  shaka.util.MediaReadyState.waitForReadyState(mediaElement,
2584
2289
  HTMLMediaElement.HAVE_CURRENT_DATA,
2585
2290
  this.loadEventManager_,
@@ -2620,9 +2325,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2620
2325
  } else if (mediaElement.preload == 'none') {
2621
2326
  shaka.log.alwaysWarn(
2622
2327
  'With <video preload="none">, the browser will not load anything ' +
2623
- 'until play() is called. We are unable to measure load latency in ' +
2624
- 'a meaningful way, and we cannot provide track info yet. Please do ' +
2625
- 'not use preload="none" with Shaka Player.');
2328
+ 'until play() is called. We are unable to measure load latency ' +
2329
+ 'in a meaningful way, and we cannot provide track info yet. ' +
2330
+ 'Please do not use preload="none" with Shaka Player.');
2626
2331
  // We can't wait for an event load loadedmetadata, since that will be
2627
2332
  // blocked until a user interaction. So resolve the Promise now.
2628
2333
  fullyLoaded.resolve();
@@ -2632,21 +2337,37 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2632
2337
  fullyLoaded.reject(this.videoErrorToShakaError_());
2633
2338
  });
2634
2339
 
2635
- return new shaka.util.AbortableOperation(fullyLoaded, /* onAbort= */ () => {
2636
- const abortedError = new shaka.util.Error(
2637
- shaka.util.Error.Severity.CRITICAL,
2638
- shaka.util.Error.Category.PLAYER,
2639
- shaka.util.Error.Code.OPERATION_ABORTED);
2640
- fullyLoaded.reject(abortedError);
2641
- return Promise.resolve(); // Abort complete.
2642
- }).chain(() => {
2643
- if (this.isLive() && this.config_.streaming.liveSync) {
2644
- const onTimeUpdate = () => this.onTimeUpdate_();
2645
- this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
2340
+ await fullyLoaded;
2341
+
2342
+ const isLive = this.isLive();
2343
+
2344
+ if (isLive && this.config_.streaming.liveSync) {
2345
+ const onTimeUpdate = () => this.onTimeUpdate_();
2346
+ this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
2347
+ } else if (!isLive) {
2348
+ const onVideoProgress = () => this.onVideoProgress_();
2349
+ this.loadEventManager_.listen(
2350
+ mediaElement, 'timeupdate', onVideoProgress);
2351
+ this.onVideoProgress_();
2352
+ }
2353
+
2354
+ if (this.adManager_) {
2355
+ this.adManager_.onManifestUpdated(isLive);
2356
+ // There is no good way to detect when the manifest has been updated,
2357
+ // so we use seekRange().end so we can tell when it has been updated.
2358
+ if (isLive) {
2359
+ let prevSeekRangeEnd = this.seekRange().end;
2360
+ this.loadEventManager_.listen(mediaElement, 'progress', () => {
2361
+ const newSeekRangeEnd = this.seekRange().end;
2362
+ if (prevSeekRangeEnd != newSeekRangeEnd) {
2363
+ this.adManager_.onManifestUpdated(this.isLive());
2364
+ prevSeekRangeEnd = newSeekRangeEnd;
2365
+ }
2366
+ });
2646
2367
  }
2368
+ }
2647
2369
 
2648
- this.fullyLoaded_ = true;
2649
- });
2370
+ this.fullyLoaded_ = true;
2650
2371
  }
2651
2372
 
2652
2373
  /**
@@ -2839,6 +2560,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2839
2560
  }
2840
2561
  }
2841
2562
 
2563
+ /**
2564
+ * Releases all of the mutexes of the player. Meant for use by the tests.
2565
+ * @export
2566
+ */
2567
+ releaseAllMutexes() {
2568
+ this.mutex_.releaseAll();
2569
+ }
2570
+
2842
2571
  /**
2843
2572
  * Create a new DrmEngine instance. This may be replaced by tests to create
2844
2573
  * fake instances. Configuration and initialization will be handled after
@@ -2859,13 +2588,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2859
2588
  * @return {!shaka.net.NetworkingEngine}
2860
2589
  */
2861
2590
  createNetworkingEngine() {
2862
- /** @type {function(number, number)} */
2863
- const onProgressUpdated_ = (deltaTimeMs, bytesDownloaded) => {
2591
+ /** @type {function(number, number, boolean)} */
2592
+ const onProgressUpdated_ = (deltaTimeMs, bytesDownloaded, allowSwitch) => {
2864
2593
  // In some situations, such as during offline storage, the abr manager
2865
2594
  // might not yet exist. Therefore, we need to check if abr manager has
2866
2595
  // been initialized before using it.
2867
2596
  if (this.abrManager_) {
2868
- this.abrManager_.segmentDownloaded(deltaTimeMs, bytesDownloaded);
2597
+ this.abrManager_.segmentDownloaded(
2598
+ deltaTimeMs, bytesDownloaded, allowSwitch);
2869
2599
  }
2870
2600
  };
2871
2601
  /** @type {shaka.net.NetworkingEngine.OnHeadersReceived} */
@@ -2894,8 +2624,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2894
2624
  this.cmcdManager_.applyData(type, request, context);
2895
2625
  };
2896
2626
 
2627
+
2628
+ const onRetry_ = (type, context, newUrl, oldUrl) => {
2629
+ if (this.parser_ && this.parser_.banLocation) {
2630
+ this.parser_.banLocation(oldUrl);
2631
+ }
2632
+ };
2633
+
2897
2634
  return new shaka.net.NetworkingEngine(
2898
- onProgressUpdated_, onHeadersReceived_, onDownloadFailed_, onRequest_);
2635
+ onProgressUpdated_, onHeadersReceived_, onDownloadFailed_, onRequest_,
2636
+ onRetry_);
2899
2637
  }
2900
2638
 
2901
2639
  /**
@@ -2979,18 +2717,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2979
2717
  * Initialize and start the buffering system (observer and timer) so that we
2980
2718
  * can monitor our buffer lead during playback.
2981
2719
  *
2720
+ * @param {!HTMLMediaElement} mediaElement
2982
2721
  * @param {number} rebufferingGoal
2983
2722
  * @private
2984
2723
  */
2985
- startBufferManagement_(rebufferingGoal) {
2724
+ startBufferManagement_(mediaElement, rebufferingGoal) {
2986
2725
  goog.asserts.assert(
2987
2726
  !this.bufferObserver_,
2988
2727
  'No buffering observer should exist before initialization.');
2989
2728
 
2990
- goog.asserts.assert(
2991
- !this.bufferPoller_,
2992
- 'No buffer timer should exist before initialization.');
2993
-
2994
2729
  // Give dummy values, will be updated below.
2995
2730
  this.bufferObserver_ = new shaka.media.BufferingObserver(1, 2);
2996
2731
 
@@ -3000,12 +2735,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3000
2735
  this.updateBufferingSettings_(rebufferingGoal);
3001
2736
  this.updateBufferState_();
3002
2737
 
3003
- // TODO: We should take some time to look into the effects of our
3004
- // quarter-second refresh practice. We often use a quarter-second
3005
- // but we have no documentation about why.
3006
- this.bufferPoller_ = new shaka.util.Timer(() => {
3007
- this.pollBufferState_();
3008
- }).tickEvery(/* seconds= */ 0.25);
2738
+ this.loadEventManager_.listen(mediaElement, 'waiting',
2739
+ (e) => this.pollBufferState_());
2740
+ this.loadEventManager_.listen(mediaElement, 'stalled',
2741
+ (e) => this.pollBufferState_());
2742
+ this.loadEventManager_.listen(mediaElement, 'canplaythrough',
2743
+ (e) => this.pollBufferState_());
2744
+ this.loadEventManager_.listen(mediaElement, 'progress',
2745
+ (e) => this.pollBufferState_());
3009
2746
  }
3010
2747
 
3011
2748
  /**
@@ -3077,16 +2814,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3077
2814
  * @param {!shaka.extern.TextDisplayer} textDisplayer
3078
2815
  * @param {!function(!Array.<shaka.extern.ID3Metadata>, number, ?number)}
3079
2816
  * onMetadata
3080
- * @param {shaka.lcevc.Dil} lcevcDil
2817
+ * @param {shaka.lcevc.Dec} lcevcDec
3081
2818
  *
3082
2819
  * @return {!shaka.media.MediaSourceEngine}
3083
2820
  */
3084
- createMediaSourceEngine(mediaElement, textDisplayer, onMetadata, lcevcDil) {
2821
+ createMediaSourceEngine(mediaElement, textDisplayer, onMetadata, lcevcDec) {
3085
2822
  return new shaka.media.MediaSourceEngine(
3086
2823
  mediaElement,
3087
2824
  textDisplayer,
3088
2825
  onMetadata,
3089
- lcevcDil);
2826
+ lcevcDec);
3090
2827
  }
3091
2828
 
3092
2829
  /**
@@ -3580,17 +3317,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3580
3317
 
3581
3318
  /**
3582
3319
  * Get the range of time (in seconds) that seeking is allowed. If the player
3583
- * has not loaded content, this will return a range from 0 to 0.
3320
+ * has not loaded content and the manifest is HLS, this will return a range
3321
+ * from 0 to 0.
3584
3322
  *
3585
3323
  * @return {{start: number, end: number}}
3586
3324
  * @export
3587
3325
  */
3588
3326
  seekRange() {
3589
- if (!this.fullyLoaded_) {
3590
- return {'start': 0, 'end': 0};
3591
- }
3592
-
3593
3327
  if (this.manifest_) {
3328
+ // With HLS lazy-loading, there were some situations where the manifest
3329
+ // had partially loaded, enough to move onto further load stages, but no
3330
+ // segments had been loaded, so the timeline is still unknown.
3331
+ // See: https://github.com/shaka-project/shaka-player/pull/4590
3332
+ if (!this.fullyLoaded_ &&
3333
+ this.manifest_.type == shaka.media.ManifestParser.HLS) {
3334
+ return {'start': 0, 'end': 0};
3335
+ }
3594
3336
  const timeline = this.manifest_.presentationTimeline;
3595
3337
 
3596
3338
  return {
@@ -3894,13 +3636,82 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3894
3636
  * @export
3895
3637
  */
3896
3638
  getImageTracks() {
3639
+ const StreamUtils = shaka.util.StreamUtils;
3640
+ let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
3897
3641
  if (this.manifest_) {
3898
- const imageStreams = this.manifest_.imageStreams;
3899
- const StreamUtils = shaka.util.StreamUtils;
3900
- return imageStreams.map((image) => StreamUtils.imageStreamToTrack(image));
3901
- } else {
3902
- return [];
3642
+ imageStreams = this.manifest_.imageStreams;
3643
+ }
3644
+ return imageStreams.map((image) => StreamUtils.imageStreamToTrack(image));
3645
+ }
3646
+
3647
+ /**
3648
+ * Returns Thumbnail objects for each thumbnail for a given image track ID.
3649
+ *
3650
+ * If the player has not loaded content, this will return a null.
3651
+ *
3652
+ * @param {number} trackId
3653
+ * @return {!Promise.<?Array<!shaka.extern.Thumbnail>>}
3654
+ * @export
3655
+ */
3656
+ async getAllThumbnails(trackId) {
3657
+ if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
3658
+ this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
3659
+ return null;
3660
+ }
3661
+ let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
3662
+ if (this.manifest_) {
3663
+ imageStreams = this.manifest_.imageStreams;
3664
+ }
3665
+ const imageStream = imageStreams.find(
3666
+ (stream) => stream.id == trackId);
3667
+ if (!imageStream) {
3668
+ return null;
3669
+ }
3670
+ if (!imageStream.segmentIndex) {
3671
+ await imageStream.createSegmentIndex();
3672
+ }
3673
+ const promises = [];
3674
+ imageStream.segmentIndex.forEachTopLevelReference((reference) => {
3675
+ const dimensions = this.parseTilesLayout_(
3676
+ reference.getTilesLayout() || imageStream.tilesLayout);
3677
+ if (dimensions) {
3678
+ const numThumbnails = dimensions.rows * dimensions.columns;
3679
+ const duration = reference.trueEndTime - reference.startTime;
3680
+ for (let i = 0; i < numThumbnails; i++) {
3681
+ const sampleTime = reference.startTime + duration * i / numThumbnails;
3682
+ promises.push(this.getThumbnails(trackId, sampleTime));
3683
+ }
3684
+ }
3685
+ });
3686
+ const thumbnails = await Promise.all(promises);
3687
+ return thumbnails.filter((t) => t);
3688
+ }
3689
+
3690
+ /**
3691
+ * Parses a tiles layout.
3692
+ *
3693
+ * @param {string|undefined} tilesLayout
3694
+ * @return {?{
3695
+ * columns: number,
3696
+ * rows: number
3697
+ * }}
3698
+ * @private
3699
+ */
3700
+ parseTilesLayout_(tilesLayout) {
3701
+ if (!tilesLayout) {
3702
+ return null;
3703
+ }
3704
+ // This expression is used to detect one or more numbers (0-9) followed
3705
+ // by an x and after one or more numbers (0-9)
3706
+ const match = /(\d+)x(\d+)/.exec(tilesLayout);
3707
+ if (!match) {
3708
+ shaka.log.warning('Tiles layout does not contain a valid format ' +
3709
+ ' (columns x rows)');
3710
+ return null;
3903
3711
  }
3712
+ const columns = parseInt(match[1], 10);
3713
+ const rows = parseInt(match[2], 10);
3714
+ return {columns, rows};
3904
3715
  }
3905
3716
 
3906
3717
  /**
@@ -3914,84 +3725,83 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3914
3725
  * @export
3915
3726
  */
3916
3727
  async getThumbnails(trackId, time) {
3728
+ if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
3729
+ this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
3730
+ return null;
3731
+ }
3732
+ let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
3917
3733
  if (this.manifest_) {
3918
- const imageStream = this.manifest_.imageStreams.find(
3919
- (stream) => stream.id == trackId);
3920
- if (!imageStream) {
3921
- return null;
3922
- }
3923
- if (!imageStream.segmentIndex) {
3924
- await imageStream.createSegmentIndex();
3925
- }
3926
- const referencePosition = imageStream.segmentIndex.find(time);
3927
- if (referencePosition == null) {
3928
- return null;
3929
- }
3930
- const reference = imageStream.segmentIndex.get(referencePosition);
3931
- const tilesLayout =
3932
- reference.getTilesLayout() || imageStream.tilesLayout;
3933
- // This expression is used to detect one or more numbers (0-9) followed
3934
- // by an x and after one or more numbers (0-9)
3935
- const match = /(\d+)x(\d+)/.exec(tilesLayout);
3936
- if (!match) {
3937
- shaka.log.warning('Tiles layout does not contain a valid format ' +
3938
- ' (columns x rows)');
3939
- return null;
3940
- }
3941
- const fullImageWidth = imageStream.width || 0;
3942
- const fullImageHeight = imageStream.height || 0;
3943
- const columns = parseInt(match[1], 10);
3944
- const rows = parseInt(match[2], 10);
3945
- let width = fullImageWidth / columns;
3946
- let height = fullImageHeight / rows;
3947
- const totalImages = columns * rows;
3948
- const segmentDuration = reference.trueEndTime - reference.startTime;
3949
- const thumbnailDuration =
3950
- reference.getTileDuration() || (segmentDuration / totalImages);
3951
- let thumbnailTime = reference.startTime;
3952
- let positionX = 0;
3953
- let positionY = 0;
3954
- // If the number of images in the segment is greater than 1, we have to
3955
- // find the correct image. For that we will return to the app the
3956
- // coordinates of the position of the correct image.
3957
- // Image search is always from left to right and top to bottom.
3958
- // Note: The time between images within the segment is always
3959
- // equidistant.
3960
- //
3961
- // Eg: Total images 5, tileLayout 5x1, segmentDuration 5, thumbnailTime 2
3962
- // positionX = 0.4 * fullImageWidth
3963
- // positionY = 0
3964
- if (totalImages > 1) {
3965
- const thumbnailPosition =
3966
- Math.floor((time - reference.startTime) / thumbnailDuration);
3967
- thumbnailTime = reference.startTime +
3968
- (thumbnailPosition * thumbnailDuration);
3969
- positionX = (thumbnailPosition % columns) * width;
3970
- positionY = Math.floor(thumbnailPosition / columns) * height;
3971
- }
3972
- let sprite = false;
3973
- const thumbnailSprite = reference.getThumbnailSprite();
3974
- if (thumbnailSprite) {
3975
- sprite = true;
3976
- height = thumbnailSprite.height;
3977
- positionX = thumbnailSprite.positionX;
3978
- positionY = thumbnailSprite.positionY;
3979
- width = thumbnailSprite.width;
3980
- }
3981
- return {
3982
- imageHeight: fullImageHeight,
3983
- imageWidth: fullImageWidth,
3984
- height: height,
3985
- positionX: positionX,
3986
- positionY: positionY,
3987
- startTime: thumbnailTime,
3988
- duration: thumbnailDuration,
3989
- uris: reference.getUris(),
3990
- width: width,
3991
- sprite: sprite,
3992
- };
3734
+ imageStreams = this.manifest_.imageStreams;
3735
+ }
3736
+ const imageStream = imageStreams.find(
3737
+ (stream) => stream.id == trackId);
3738
+ if (!imageStream) {
3739
+ return null;
3740
+ }
3741
+ if (!imageStream.segmentIndex) {
3742
+ await imageStream.createSegmentIndex();
3743
+ }
3744
+ const referencePosition = imageStream.segmentIndex.find(time);
3745
+ if (referencePosition == null) {
3746
+ return null;
3747
+ }
3748
+ const reference = imageStream.segmentIndex.get(referencePosition);
3749
+ const dimensions = this.parseTilesLayout_(
3750
+ reference.getTilesLayout() || imageStream.tilesLayout);
3751
+ if (!dimensions) {
3752
+ return null;
3753
+ }
3754
+ const fullImageWidth = imageStream.width || 0;
3755
+ const fullImageHeight = imageStream.height || 0;
3756
+ let width = fullImageWidth / dimensions.columns;
3757
+ let height = fullImageHeight / dimensions.rows;
3758
+ const totalImages = dimensions.columns * dimensions.rows;
3759
+ const segmentDuration = reference.trueEndTime - reference.startTime;
3760
+ const thumbnailDuration =
3761
+ reference.getTileDuration() || (segmentDuration / totalImages);
3762
+ let thumbnailTime = reference.startTime;
3763
+ let positionX = 0;
3764
+ let positionY = 0;
3765
+ // If the number of images in the segment is greater than 1, we have to
3766
+ // find the correct image. For that we will return to the app the
3767
+ // coordinates of the position of the correct image.
3768
+ // Image search is always from left to right and top to bottom.
3769
+ // Note: The time between images within the segment is always
3770
+ // equidistant.
3771
+ //
3772
+ // Eg: Total images 5, tileLayout 5x1, segmentDuration 5, thumbnailTime 2
3773
+ // positionX = 0.4 * fullImageWidth
3774
+ // positionY = 0
3775
+ if (totalImages > 1) {
3776
+ const thumbnailPosition =
3777
+ Math.floor((time - reference.startTime) / thumbnailDuration);
3778
+ thumbnailTime = reference.startTime +
3779
+ (thumbnailPosition * thumbnailDuration);
3780
+ positionX = (thumbnailPosition % dimensions.columns) * width;
3781
+ positionY = Math.floor(thumbnailPosition / dimensions.columns) * height;
3782
+ }
3783
+ let sprite = false;
3784
+ const thumbnailSprite = reference.getThumbnailSprite();
3785
+ if (thumbnailSprite) {
3786
+ sprite = true;
3787
+ height = thumbnailSprite.height;
3788
+ positionX = thumbnailSprite.positionX;
3789
+ positionY = thumbnailSprite.positionY;
3790
+ width = thumbnailSprite.width;
3993
3791
  }
3994
- return null;
3792
+ return {
3793
+ segment: reference,
3794
+ imageHeight: fullImageHeight,
3795
+ imageWidth: fullImageWidth,
3796
+ height: height,
3797
+ positionX: positionX,
3798
+ positionY: positionY,
3799
+ startTime: thumbnailTime,
3800
+ duration: thumbnailDuration,
3801
+ uris: reference.getUris(),
3802
+ width: width,
3803
+ sprite: sprite,
3804
+ };
3995
3805
  }
3996
3806
 
3997
3807
  /**
@@ -4108,7 +3918,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4108
3918
  // When track is selected, back-propagate the language to
4109
3919
  // currentAudioLanguage_.
4110
3920
  this.currentAdaptationSetCriteria_ = new shaka.media.ExampleBasedCriteria(
4111
- variant);
3921
+ variant,
3922
+ this.config_.mediaSource.codecSwitchingStrategy,
3923
+ this.config_.manifest.dash.enableAudioGroups);
4112
3924
 
4113
3925
  // Update AbrManager variants to match these new settings.
4114
3926
  this.updateAbrManagerVariants_();
@@ -4184,8 +3996,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4184
3996
  selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0) {
4185
3997
  if (this.manifest_ && this.playhead_) {
4186
3998
  this.currentAdaptationSetCriteria_ =
4187
- new shaka.media.PreferenceBasedCriteria(language, role || '',
4188
- channelsCount, /* hdrLevel= */ '', /* label= */ '');
3999
+ new shaka.media.PreferenceBasedCriteria(
4000
+ language,
4001
+ role || '',
4002
+ channelsCount,
4003
+ /* hdrLevel= */ '',
4004
+ /* videoLayout= */ '',
4005
+ /* label= */ '',
4006
+ this.config_.mediaSource.codecSwitchingStrategy,
4007
+ this.config_.manifest.dash.enableAudioGroups);
4189
4008
 
4190
4009
  const diff = (a, b) => {
4191
4010
  if (!a.video && !b.video) {
@@ -4302,7 +4121,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4302
4121
  // label have the same language.
4303
4122
  this.currentAdaptationSetCriteria_ =
4304
4123
  new shaka.media.PreferenceBasedCriteria(
4305
- firstVariantWithLabel.language, '', 0, '', label);
4124
+ firstVariantWithLabel.language,
4125
+ /* role= */ '',
4126
+ /* channelCount= */ 0,
4127
+ /* hdrLevel= */ '',
4128
+ /* videoLayout= */ '',
4129
+ label,
4130
+ this.config_.mediaSource.codecSwitchingStrategy,
4131
+ this.config_.manifest.dash.enableAudioGroups);
4306
4132
 
4307
4133
  this.chooseVariantAndSwitch_(clearBuffer, safeMargin);
4308
4134
  } else if (this.video_ && this.video_.audioTracks) {
@@ -4330,7 +4156,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4330
4156
  isTextTrackVisible() {
4331
4157
  const expected = this.isTextVisible_;
4332
4158
 
4333
- if (this.mediaSourceEngine_) {
4159
+ if (this.mediaSourceEngine_ &&
4160
+ this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
4334
4161
  // Make sure our values are still in-sync.
4335
4162
  const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible();
4336
4163
  goog.asserts.assert(
@@ -4371,6 +4198,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4371
4198
  * @export
4372
4199
  */
4373
4200
  getChapters(language) {
4201
+ if (!this.video_ || !this.video_.src || !this.video_.textTracks) {
4202
+ return [];
4203
+ }
4374
4204
  const LanguageUtils = shaka.util.LanguageUtils;
4375
4205
  const inputlanguage = LanguageUtils.normalize(language);
4376
4206
  const chaptersTracks = this.getChaptersTracks_();
@@ -4529,22 +4359,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4529
4359
  return null;
4530
4360
  }
4531
4361
 
4532
- const walkerPayload = this.walker_.getCurrentPayload();
4533
-
4534
4362
  let presentationTime = 0;
4535
4363
  if (this.playhead_) {
4536
4364
  presentationTime = this.playhead_.getTime();
4537
- } else if (walkerPayload) {
4538
- if (walkerPayload.startTime == null) {
4539
- // A live stream with no requested start time and no playhead yet. We
4540
- // would start at the live edge, but we don't have that yet, so return
4541
- // the current date & time.
4542
- return new Date();
4543
- } else {
4544
- // A specific start time has been requested. This is what Playhead will
4545
- // use once it is created.
4546
- presentationTime = walkerPayload.startTime;
4547
- }
4365
+ } else if (this.startTime_ == null) {
4366
+ // A live stream with no requested start time and no playhead yet. We
4367
+ // would start at the live edge, but we don't have that yet, so return
4368
+ // the current date & time.
4369
+ return new Date();
4370
+ } else {
4371
+ // A specific start time has been requested. This is what Playhead will
4372
+ // use once it is created.
4373
+ presentationTime = this.startTime_;
4548
4374
  }
4549
4375
 
4550
4376
  if (this.manifest_) {
@@ -4759,9 +4585,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4759
4585
 
4760
4586
  let adCuePoints = [];
4761
4587
  if (this.adManager_) {
4762
- try {
4763
- adCuePoints = this.adManager_.getServerSideCuePoints();
4764
- } catch (error) {}
4588
+ adCuePoints = this.adManager_.getCuePoints();
4765
4589
  }
4766
4590
 
4767
4591
  if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
@@ -4792,7 +4616,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4792
4616
 
4793
4617
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
4794
4618
 
4795
- const duration = this.manifest_.presentationTimeline.getDuration();
4619
+ let duration = this.video_.duration;
4620
+ if (this.manifest_) {
4621
+ duration = this.manifest_.presentationTimeline.getDuration();
4622
+ }
4796
4623
  if (duration == Infinity) {
4797
4624
  throw new shaka.util.Error(
4798
4625
  shaka.util.Error.Severity.RECOVERABLE,
@@ -4816,6 +4643,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4816
4643
  const stream = {
4817
4644
  id: this.nextExternalStreamId_++,
4818
4645
  originalId: null,
4646
+ groupId: null,
4819
4647
  createSegmentIndex: () => Promise.resolve(),
4820
4648
  segmentIndex: shaka.media.SegmentIndex.forSingleSegment(
4821
4649
  /* startTime= */ 0,
@@ -4842,6 +4670,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4842
4670
  closedCaptions: null,
4843
4671
  accessibilityPurpose: null,
4844
4672
  external: true,
4673
+ fastSwitching: false,
4845
4674
  };
4846
4675
 
4847
4676
  const fullMimeType = shaka.util.MimeUtils.getFullType(
@@ -4885,14 +4714,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4885
4714
  shaka.util.Error.Code.CONTENT_NOT_LOADED);
4886
4715
  }
4887
4716
 
4888
- if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
4889
- shaka.log.error('Cannot add this thumbnail track when loaded with src=');
4890
- throw new shaka.util.Error(
4891
- shaka.util.Error.Severity.RECOVERABLE,
4892
- shaka.util.Error.Category.TEXT,
4893
- shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_SRC_EQUALS);
4894
- }
4895
-
4896
4717
  if (!mimeType) {
4897
4718
  mimeType = await this.getTextMimetype_(uri);
4898
4719
  }
@@ -4906,8 +4727,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4906
4727
  }
4907
4728
 
4908
4729
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
4909
-
4910
- const duration = this.manifest_.presentationTimeline.getDuration();
4730
+ let duration = this.video_.duration;
4731
+ if (this.manifest_) {
4732
+ duration = this.manifest_.presentationTimeline.getDuration();
4733
+ }
4911
4734
  if (duration == Infinity) {
4912
4735
  throw new shaka.util.Error(
4913
4736
  shaka.util.Error.Severity.RECOVERABLE,
@@ -4937,7 +4760,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4937
4760
  vttOffset: 0,
4938
4761
  };
4939
4762
  const data = shaka.util.BufferUtils.toUint8(buffer);
4940
- const cues = TextParser.parseMedia(data, time);
4763
+ const cues = TextParser.parseMedia(data, time, uri);
4941
4764
 
4942
4765
  const references = [];
4943
4766
  for (const cue of cues) {
@@ -4972,6 +4795,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4972
4795
  const stream = {
4973
4796
  id: this.nextExternalStreamId_++,
4974
4797
  originalId: null,
4798
+ groupId: null,
4975
4799
  createSegmentIndex: () => Promise.resolve(),
4976
4800
  segmentIndex: new shaka.media.SegmentIndex(references),
4977
4801
  mimeType: mimeType || '',
@@ -4996,9 +4820,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4996
4820
  tilesLayout: '1x1',
4997
4821
  accessibilityPurpose: null,
4998
4822
  external: true,
4823
+ fastSwitching: false,
4999
4824
  };
5000
4825
 
5001
- this.manifest_.imageStreams.push(stream);
4826
+ if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
4827
+ this.externalSrcEqualsThumbnailsStreams_.push(stream);
4828
+ } else {
4829
+ this.manifest_.imageStreams.push(stream);
4830
+ }
5002
4831
  this.onTracksChanged_();
5003
4832
  return shaka.util.StreamUtils.imageStreamToTrack(stream);
5004
4833
  }
@@ -5033,9 +4862,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5033
4862
 
5034
4863
  let adCuePoints = [];
5035
4864
  if (this.adManager_) {
5036
- try {
5037
- adCuePoints = this.adManager_.getServerSideCuePoints();
5038
- } catch (error) {}
4865
+ adCuePoints = this.adManager_.getCuePoints();
5039
4866
  }
5040
4867
 
5041
4868
  /** @type {!HTMLTrackElement} */
@@ -5062,6 +4889,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5062
4889
  });
5063
4890
  });
5064
4891
 
4892
+ this.onTracksChanged_();
4893
+
5065
4894
  return chaptersTrack;
5066
4895
  }
5067
4896
 
@@ -5080,19 +4909,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5080
4909
  * @private
5081
4910
  */
5082
4911
  async getTextMimetype_(uri) {
5083
- // Try using the uri extension.
5084
- const extension = shaka.media.ManifestParser.getExtension(uri);
5085
- let mimeType = shaka.Player.TEXT_EXTENSIONS_TO_MIME_TYPES_[extension];
5086
-
5087
- if (mimeType) {
5088
- return mimeType;
5089
- }
5090
-
4912
+ let mimeType;
5091
4913
  try {
5092
4914
  goog.asserts.assert(
5093
4915
  this.networkingEngine_, 'Need networking engine.');
5094
4916
  // eslint-disable-next-line require-atomic-updates
5095
- mimeType = await shaka.media.ManifestParser.getMimeType(uri,
4917
+ mimeType = await shaka.net.NetworkingUtils.getMimeType(uri,
5096
4918
  this.networkingEngine_,
5097
4919
  this.config_.streaming.retryParameters);
5098
4920
  } catch (error) {}
@@ -5103,12 +4925,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5103
4925
 
5104
4926
  shaka.log.error(
5105
4927
  'The mimeType has not been provided and it could not be deduced ' +
5106
- 'from its extension.');
4928
+ 'from its uri.');
5107
4929
  throw new shaka.util.Error(
5108
4930
  shaka.util.Error.Severity.RECOVERABLE,
5109
4931
  shaka.util.Error.Category.TEXT,
5110
4932
  shaka.util.Error.Code.TEXT_COULD_NOT_GUESS_MIME_TYPE,
5111
- extension);
4933
+ uri);
5112
4934
  }
5113
4935
 
5114
4936
  /**
@@ -5197,7 +5019,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5197
5019
  vttOffset: 0,
5198
5020
  };
5199
5021
  const data = shaka.util.BufferUtils.toUint8(buffer);
5200
- const cues = obj.parseMedia(data, time);
5022
+ const cues = obj.parseMedia(data, time, /* uri= */ null);
5201
5023
  return shaka.text.WebVttGenerator.convert(cues, adCuePoints);
5202
5024
  }
5203
5025
  throw new shaka.util.Error(
@@ -5316,7 +5138,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5316
5138
  return new shaka.text.UITextDisplayer(
5317
5139
  this.video_, this.videoContainer_);
5318
5140
  } else {
5319
- return new shaka.text.SimpleTextDisplayer(this.video_);
5141
+ // eslint-disable-next-line no-restricted-syntax
5142
+ if (HTMLMediaElement.prototype.addTextTrack) {
5143
+ return new shaka.text.SimpleTextDisplayer(this.video_);
5144
+ } else {
5145
+ shaka.log.warning('Text tracks are not supported by the ' +
5146
+ 'browser, disabling.');
5147
+ return new shaka.text.StubTextDisplayer();
5148
+ }
5320
5149
  }
5321
5150
  };
5322
5151
  return config;
@@ -5396,6 +5225,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5396
5225
  const textStream = {
5397
5226
  id: this.nextExternalStreamId_++, // A globally unique ID.
5398
5227
  originalId: id, // The CC ID string, like 'CC1', 'CC3', etc.
5228
+ groupId: null,
5399
5229
  createSegmentIndex: () => Promise.resolve(),
5400
5230
  segmentIndex,
5401
5231
  mimeType,
@@ -5419,6 +5249,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5419
5249
  closedCaptions: null,
5420
5250
  accessibilityPurpose: null,
5421
5251
  external: false,
5252
+ fastSwitching: false,
5422
5253
  };
5423
5254
  manifest.textStreams.push(textStream);
5424
5255
  closedCaptionsSet.add(id);
@@ -5447,14 +5278,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5447
5278
  */
5448
5279
  async filterManifestWithStreamUtils_(manifest) {
5449
5280
  goog.asserts.assert(manifest, 'Manifest should exist!');
5450
- goog.asserts.assert(this.video_, 'Must not be destroyed');
5451
-
5452
- /** @type {?shaka.extern.Variant} */
5453
- const currentVariant = this.streamingEngine_ ?
5454
- this.streamingEngine_.getCurrentVariant() : null;
5455
-
5456
- await shaka.util.StreamUtils.filterManifest(
5457
- this.drmEngine_, currentVariant, manifest);
5281
+ await shaka.util.StreamUtils.filterManifest(this.drmEngine_, manifest);
5458
5282
  this.checkPlayableVariants_(manifest);
5459
5283
  }
5460
5284
 
@@ -5511,21 +5335,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5511
5335
  }
5512
5336
  }
5513
5337
 
5514
- /**
5515
- * @private
5516
- */
5517
- filterManifestByCurrentVariant_() {
5518
- goog.asserts.assert(this.manifest_, 'Manifest should be valid');
5519
- goog.asserts.assert(this.streamingEngine_,
5520
- 'StreamingEngine should be valid');
5521
-
5522
- const currentVariant = this.streamingEngine_ ?
5523
- this.streamingEngine_.getCurrentVariant() : null;
5524
- shaka.util.StreamUtils.filterManifestByCurrentVariant(currentVariant,
5525
- this.manifest_);
5526
- this.checkPlayableVariants_(this.manifest_);
5527
- }
5528
-
5529
5338
  /**
5530
5339
  * @param {shaka.extern.Variant} initialVariant
5531
5340
  * @param {number} time
@@ -5594,7 +5403,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5594
5403
  const loaded = this.stats_ && this.bufferObserver_ && this.playhead_;
5595
5404
 
5596
5405
  if (loaded) {
5597
- this.playRateController_.setBuffering(isBuffering);
5598
5406
  if (this.cmcdManager_) {
5599
5407
  this.cmcdManager_.setBuffering(isBuffering);
5600
5408
  }
@@ -5661,14 +5469,20 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5661
5469
 
5662
5470
  const history = this.stats_.getStateHistory();
5663
5471
 
5472
+ let updateState = 'playing';
5664
5473
  if (this.bufferObserver_.getState() == State.STARVING) {
5665
- history.update('buffering');
5474
+ updateState = 'buffering';
5666
5475
  } else if (this.video_.paused) {
5667
- history.update('paused');
5476
+ updateState = 'paused';
5668
5477
  } else if (this.video_.ended) {
5669
- history.update('ended');
5670
- } else {
5671
- history.update('playing');
5478
+ updateState = 'ended';
5479
+ }
5480
+ const stateChanged = history.update(updateState);
5481
+
5482
+ if (stateChanged) {
5483
+ const eventName = shaka.util.FakeEvent.EventName.StateChanged;
5484
+ const data = (new Map()).set('newstate', updateState);
5485
+ this.dispatchEvent(this.makeEvent_(eventName, data));
5672
5486
  }
5673
5487
  }
5674
5488
 
@@ -5709,6 +5523,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5709
5523
  }
5710
5524
  }
5711
5525
 
5526
+ let liveSyncMinLatency;
5527
+ let liveSyncMinPlaybackRate;
5528
+ if (this.config_.streaming.liveSync) {
5529
+ liveSyncMinLatency = this.config_.streaming.liveSyncMinLatency;
5530
+ liveSyncMinPlaybackRate = this.config_.streaming.liveSyncMinPlaybackRate;
5531
+ } else {
5532
+ // serviceDescription must override if it is defined in the MPD and
5533
+ // liveSync configuration is not set.
5534
+ if (this.manifest_ && this.manifest_.serviceDescription) {
5535
+ liveSyncMinLatency = this.manifest_.serviceDescription.minLatency ||
5536
+ this.config_.streaming.liveSyncMinLatency;
5537
+ liveSyncMinPlaybackRate =
5538
+ this.manifest_.serviceDescription.minPlaybackRate ||
5539
+ this.config_.streaming.liveSyncMinPlaybackRate;
5540
+ }
5541
+ }
5542
+
5712
5543
  const playbackRate = this.video_.playbackRate;
5713
5544
  const latency = seekRange.end - this.video_.currentTime;
5714
5545
  let offset = 0;
@@ -5732,11 +5563,62 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5732
5563
  'Updating playbackRate to ' + liveSyncPlaybackRate);
5733
5564
  this.trickPlay(liveSyncPlaybackRate);
5734
5565
  }
5735
- } else if (playbackRate !== 1 && playbackRate !== 0) {
5566
+ } else if (liveSyncMinLatency && liveSyncMinPlaybackRate &&
5567
+ (latency - offset) < liveSyncMinLatency) {
5568
+ if (playbackRate != liveSyncMinPlaybackRate) {
5569
+ shaka.log.debug('Latency (' + latency + 's) ' +
5570
+ 'is smaller than liveSyncMinLatency (' + liveSyncMinLatency + 's). ' +
5571
+ 'Updating playbackRate to ' + liveSyncMinPlaybackRate);
5572
+ this.trickPlay(liveSyncMinPlaybackRate);
5573
+ }
5574
+ } else if (playbackRate !== this.playRateController_.getDefaultRate()) {
5736
5575
  this.cancelTrickPlay();
5737
5576
  }
5738
5577
  }
5739
5578
 
5579
+ /**
5580
+ * Callback for video progress events
5581
+ *
5582
+ * @private
5583
+ */
5584
+ onVideoProgress_() {
5585
+ if (!this.video_) {
5586
+ return;
5587
+ }
5588
+ let hasNewCompletionPercent = false;
5589
+ const completionRatio = this.video_.currentTime / this.video_.duration;
5590
+ if (!isNaN(completionRatio)) {
5591
+ const percent = Math.round(100 * completionRatio);
5592
+ if (isNaN(this.completionPercent_)) {
5593
+ this.completionPercent_ = percent;
5594
+ hasNewCompletionPercent = true;
5595
+ } else {
5596
+ const newCompletionPercent = Math.max(this.completionPercent_, percent);
5597
+ if (this.completionPercent_ != newCompletionPercent) {
5598
+ this.completionPercent_ = newCompletionPercent;
5599
+ hasNewCompletionPercent = true;
5600
+ }
5601
+ }
5602
+ }
5603
+ if (hasNewCompletionPercent) {
5604
+ let event;
5605
+ if (this.completionPercent_ == 0) {
5606
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.Started);
5607
+ } else if (this.completionPercent_ == 25) {
5608
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.FirstQuartile);
5609
+ } else if (this.completionPercent_ == 50) {
5610
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.Midpoint);
5611
+ } else if (this.completionPercent_ == 75) {
5612
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.ThirdQuartile);
5613
+ } else if (this.completionPercent_ == 100) {
5614
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.Complete);
5615
+ }
5616
+ if (event) {
5617
+ this.dispatchEvent(event);
5618
+ }
5619
+ }
5620
+ }
5621
+
5740
5622
  /**
5741
5623
  * Callback from Playhead.
5742
5624
  *
@@ -5791,12 +5673,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5791
5673
  *
5792
5674
  * On error, this dispatches an error event and returns null.
5793
5675
  *
5676
+ * @param {boolean=} initialSelection
5794
5677
  * @return {?shaka.extern.Variant}
5795
5678
  * @private
5796
5679
  */
5797
- chooseVariant_() {
5680
+ chooseVariant_(initialSelection = false) {
5798
5681
  if (this.updateAbrManagerVariants_()) {
5799
- return this.abrManager_.chooseVariant();
5682
+ return this.abrManager_.chooseVariant(initialSelection);
5800
5683
  } else {
5801
5684
  return null;
5802
5685
  }
@@ -6132,8 +6015,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6132
6015
  const data = new Map()
6133
6016
  .set('oldTrack', from)
6134
6017
  .set('newTrack', to);
6135
- if (this.lcevcDil_) {
6136
- this.lcevcDil_.updateVariant(to);
6018
+ if (this.lcevcDec_) {
6019
+ this.lcevcDec_.updateVariant(to, this.getManifestType());
6137
6020
  }
6138
6021
  const event =
6139
6022
  this.makeEvent_(shaka.util.FakeEvent.EventName.Adaptation, data);
@@ -6163,8 +6046,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6163
6046
  const data = new Map()
6164
6047
  .set('oldTrack', from)
6165
6048
  .set('newTrack', to);
6166
- if (this.lcevcDil_) {
6167
- this.lcevcDil_.updateVariant(to);
6049
+ if (this.lcevcDec_) {
6050
+ this.lcevcDec_.updateVariant(to, this.getManifestType());
6168
6051
  }
6169
6052
 
6170
6053
  const event =
@@ -6445,6 +6328,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6445
6328
  return;
6446
6329
  }
6447
6330
 
6331
+ const event =
6332
+ this.makeEvent_(shaka.util.FakeEvent.EventName.KeyStatusChanged);
6333
+ this.dispatchEvent(event);
6334
+
6448
6335
  const keyIds = Object.keys(keyStatusMap);
6449
6336
  if (keyIds.length == 0) {
6450
6337
  shaka.log.warning(
@@ -6833,8 +6720,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6833
6720
  // Because Safari's native HLS reports slightly inaccurate values for
6834
6721
  // bufferEnd here, we use a fudge factor. Without this, we can end up in a
6835
6722
  // buffering state at the end of the stream. See issue #2117.
6836
- // TODO: Try to remove the fudge here once we no longer manage buffering
6837
- // state above the browser with playbackRate=0.
6838
6723
  const fudge = 1; // 1000 ms
6839
6724
  return bufferEnd != null && bufferEnd >= this.video_.duration - fudge;
6840
6725
  }
@@ -6851,290 +6736,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6851
6736
  shaka.util.Error.Category.PLAYER,
6852
6737
  shaka.util.Error.Code.LOAD_INTERRUPTED);
6853
6738
  }
6854
-
6855
- /**
6856
- * Key
6857
- * ----------------------
6858
- * D : Detach Node
6859
- * A : Attach Node
6860
- * MS : Media Source Node
6861
- * P : Manifest Parser Node
6862
- * M : Manifest Node
6863
- * DRM : Drm Engine Node
6864
- * L : Load Node
6865
- * U : Unloading Node
6866
- * SRC : Src Equals Node
6867
- *
6868
- * Graph Topology
6869
- * ----------------------
6870
- *
6871
- * [SRC]-----+
6872
- * ^ |
6873
- * | v
6874
- * [D]<-->[A]<-----[U]
6875
- * | ^
6876
- * v |
6877
- * [MS]------+
6878
- * | |
6879
- * v |
6880
- * [P]-------+
6881
- * | |
6882
- * v |
6883
- * [M]-------+
6884
- * | |
6885
- * v |
6886
- * [DRM]-----+
6887
- * | |
6888
- * v |
6889
- * [L]-------+
6890
- *
6891
- * @param {!shaka.routing.Node} currentlyAt
6892
- * @param {shaka.routing.Payload} currentlyWith
6893
- * @param {!shaka.routing.Node} wantsToBeAt
6894
- * @param {shaka.routing.Payload} wantsToHave
6895
- * @return {?shaka.routing.Node}
6896
- * @private
6897
- */
6898
- getNextStep_(currentlyAt, currentlyWith, wantsToBeAt, wantsToHave) {
6899
- let next = null;
6900
-
6901
- // Detach is very simple, either stay in detach (because |detach| was called
6902
- // while in detached) or go somewhere that requires us to attach to an
6903
- // element.
6904
- if (currentlyAt == this.detachNode_) {
6905
- next = wantsToBeAt == this.detachNode_ ?
6906
- this.detachNode_ :
6907
- this.attachNode_;
6908
- }
6909
-
6910
- if (currentlyAt == this.attachNode_) {
6911
- next = this.getNextAfterAttach_(wantsToBeAt, currentlyWith, wantsToHave);
6912
- }
6913
-
6914
- if (currentlyAt == this.mediaSourceNode_) {
6915
- next = this.getNextAfterMediaSource_(
6916
- wantsToBeAt, currentlyWith, wantsToHave);
6917
- }
6918
-
6919
- if (currentlyAt == this.parserNode_) {
6920
- next = this.getNextMatchingAllDependencies_(
6921
- /* destination= */ this.loadNode_,
6922
- /* next= */ this.manifestNode_,
6923
- /* reset= */ this.unloadNode_,
6924
- /* goingTo= */ wantsToBeAt,
6925
- /* has= */ currentlyWith,
6926
- /* wants= */ wantsToHave);
6927
- }
6928
-
6929
- if (currentlyAt == this.manifestNode_) {
6930
- next = this.getNextMatchingAllDependencies_(
6931
- /* destination= */ this.loadNode_,
6932
- /* next= */ this.drmNode_,
6933
- /* reset= */ this.unloadNode_,
6934
- /* goingTo= */ wantsToBeAt,
6935
- /* has= */ currentlyWith,
6936
- /* wants= */ wantsToHave);
6937
- }
6938
-
6939
- // For DRM, we have two options "load" or "unload". If all our constraints
6940
- // are met, we can go to "load". If anything is off, we must go back to
6941
- // "unload" to reset.
6942
- if (currentlyAt == this.drmNode_) {
6943
- next = this.getNextMatchingAllDependencies_(
6944
- /* destination= */ this.loadNode_,
6945
- /* next= */ this.loadNode_,
6946
- /* reset= */ this.unloadNode_,
6947
- /* goingTo= */ wantsToBeAt,
6948
- /* has= */ currentlyWith,
6949
- /* wants= */ wantsToHave);
6950
- }
6951
-
6952
- // For DRM w/ src= playback, we only care about destination and media
6953
- // element.
6954
- if (currentlyAt == this.srcEqualsDrmNode_) {
6955
- if (wantsToBeAt == this.srcEqualsNode_ &&
6956
- currentlyWith.mediaElement == wantsToHave.mediaElement) {
6957
- next = this.srcEqualsNode_;
6958
- } else {
6959
- next = this.unloadNode_;
6960
- }
6961
- }
6962
-
6963
- // After we load content, always go through unload because we can't safely
6964
- // use components after we have started playback.
6965
- if (currentlyAt == this.loadNode_ || currentlyAt == this.srcEqualsNode_) {
6966
- next = this.unloadNode_;
6967
- }
6968
-
6969
- if (currentlyAt == this.unloadNode_) {
6970
- next = this.getNextAfterUnload_(wantsToBeAt, currentlyWith, wantsToHave);
6971
- }
6972
-
6973
- goog.asserts.assert(next, 'Missing next step!');
6974
- return next;
6975
- }
6976
-
6977
- /**
6978
- * @param {!shaka.routing.Node} goingTo
6979
- * @param {shaka.routing.Payload} has
6980
- * @param {shaka.routing.Payload} wants
6981
- * @return {?shaka.routing.Node}
6982
- * @private
6983
- */
6984
- getNextAfterAttach_(goingTo, has, wants) {
6985
- // Attach and detach are the only two nodes that we can directly go
6986
- // back-and-forth between.
6987
- if (goingTo == this.detachNode_) {
6988
- return this.detachNode_;
6989
- }
6990
-
6991
- // If we are going anywhere other than detach, then we need the media
6992
- // element to match, if they don't match, we need to go through detach
6993
- // first.
6994
- if (has.mediaElement != wants.mediaElement) {
6995
- return this.detachNode_;
6996
- }
6997
-
6998
- // If we are already in attached, and someone calls |attach| again (to the
6999
- // same video element), we can handle the redundant request by re-entering
7000
- // our current state.
7001
- if (goingTo == this.attachNode_) {
7002
- return this.attachNode_;
7003
- }
7004
-
7005
- // The next step from attached to loaded is through media source.
7006
- if (goingTo == this.mediaSourceNode_ || goingTo == this.loadNode_) {
7007
- return this.mediaSourceNode_;
7008
- }
7009
-
7010
- // If we are going to src=, then we should set up DRM first. This will
7011
- // support cases like FairPlay HLS on Safari.
7012
- if (goingTo == this.srcEqualsNode_) {
7013
- return this.srcEqualsDrmNode_;
7014
- }
7015
-
7016
- // We are missing a rule, the null will get caught by a common check in
7017
- // the routing system.
7018
- return null;
7019
- }
7020
-
7021
- /**
7022
- * @param {!shaka.routing.Node} goingTo
7023
- * @param {shaka.routing.Payload} has
7024
- * @param {shaka.routing.Payload} wants
7025
- * @return {?shaka.routing.Node}
7026
- * @private
7027
- */
7028
- getNextAfterMediaSource_(goingTo, has, wants) {
7029
- // We can only go to parse manifest or unload. If we want to go to load and
7030
- // we have the right media element, we can go to parse manifest. If we
7031
- // don't, no matter where we want to go, we must go through unload.
7032
- if (goingTo == this.loadNode_ && has.mediaElement == wants.mediaElement) {
7033
- return this.parserNode_;
7034
- }
7035
-
7036
- // Right now the unload node is responsible for tearing down all playback
7037
- // components (including media source). So since we have created media
7038
- // source, we need to unload since our dependencies are not compatible.
7039
- //
7040
- // TODO: We are structured this way to maintain a historic structure. Going
7041
- // forward, there is no reason to restrict ourselves to this. Going
7042
- // forward we should explore breaking apart |onUnload| and develop
7043
- // more meaningful terminology around tearing down playback resources.
7044
- return this.unloadNode_;
7045
- }
7046
-
7047
- /**
7048
- * After unload there are only two options, attached or detached. This choice
7049
- * is based on whether or not we have a media element. If we have a media
7050
- * element, then we go to attach. If we don't have a media element, we go to
7051
- * detach.
7052
- *
7053
- * @param {!shaka.routing.Node} goingTo
7054
- * @param {shaka.routing.Payload} has
7055
- * @param {shaka.routing.Payload} wants
7056
- * @return {?shaka.routing.Node}
7057
- * @private
7058
- */
7059
- getNextAfterUnload_(goingTo, has, wants) {
7060
- // If we don't want a media element, detach.
7061
- // If we have the wrong media element, detach.
7062
- // Otherwise it means we want to attach to a media element and it is safe to
7063
- // do so.
7064
- return !wants.mediaElement || has.mediaElement != wants.mediaElement ?
7065
- this.detachNode_ :
7066
- this.attachNode_;
7067
- }
7068
-
7069
- /**
7070
- * A general method used to handle routing when we can either than one step
7071
- * toward our destination (while all our dependencies match) or go to a node
7072
- * that will reset us so we can try again.
7073
- *
7074
- * @param {!shaka.routing.Node} destinationNode
7075
- * What |goingTo| must be for us to step toward |nextNode|. Otherwise we
7076
- * will go to |resetNode|.
7077
- * @param {!shaka.routing.Node} nextNode
7078
- * The node we will go to next if |goingTo == destinationNode| and all
7079
- * dependencies match.
7080
- * @param {!shaka.routing.Node} resetNode
7081
- * The node we will go to next if |goingTo != destinationNode| or any
7082
- * dependency does not match.
7083
- * @param {!shaka.routing.Node} goingTo
7084
- * The node that the walker is trying to go to.
7085
- * @param {shaka.routing.Payload} has
7086
- * The payload that the walker currently has.
7087
- * @param {shaka.routing.Payload} wants
7088
- * The payload that the walker wants to have when iy gets to |goingTo|.
7089
- * @return {shaka.routing.Node}
7090
- * @private
7091
- */
7092
- getNextMatchingAllDependencies_(destinationNode, nextNode, resetNode, goingTo,
7093
- has, wants) {
7094
- if (goingTo == destinationNode &&
7095
- has.mediaElement == wants.mediaElement &&
7096
- has.uri == wants.uri &&
7097
- has.mimeType == wants.mimeType) {
7098
- return nextNode;
7099
- }
7100
-
7101
- return resetNode;
7102
- }
7103
-
7104
- /**
7105
- * @return {shaka.routing.Payload}
7106
- * @private
7107
- */
7108
- static createEmptyPayload_() {
7109
- return {
7110
- mediaElement: null,
7111
- mimeType: null,
7112
- startTime: null,
7113
- startTimeOfLoad: NaN,
7114
- uri: null,
7115
- };
7116
- }
7117
-
7118
- /**
7119
- * Using a promise, wrap the listeners returned by |Walker.startNewRoute|.
7120
- * This will work for most usages in |Player| but should not be used for
7121
- * special cases.
7122
- *
7123
- * This will connect |onCancel|, |onEnd|, |onError|, and |onSkip| with
7124
- * |resolve| and |reject| but will leave |onStart| unset.
7125
- *
7126
- * @param {shaka.routing.Walker.Listeners} listeners
7127
- * @return {!Promise}
7128
- * @private
7129
- */
7130
- wrapWalkerListenersWithPromise_(listeners) {
7131
- return new Promise((resolve, reject) => {
7132
- listeners.onCancel = () => reject(this.createAbortLoadError_());
7133
- listeners.onEnd = () => resolve();
7134
- listeners.onError = (e) => reject(e);
7135
- listeners.onSkip = () => reject(this.createAbortLoadError_());
7136
- });
7137
- }
7138
6739
  };
7139
6740
 
7140
6741
  /**
@@ -7175,7 +6776,8 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
7175
6776
  * @define {string} A version number taken from git at compile time.
7176
6777
  * @export
7177
6778
  */
7178
- shaka.Player.version = 'v4.4.3-uncompiled';
6779
+ // eslint-disable-next-line no-useless-concat
6780
+ shaka.Player.version = 'v4.6.0' + '-uncompiled'; // x-release-please-version
7179
6781
 
7180
6782
  // Initialize the deprecation system using the version string we just set
7181
6783
  // on the player.
@@ -7202,47 +6804,6 @@ shaka.Player.supportPlugins_ = {};
7202
6804
  shaka.Player.adManagerFactory_ = null;
7203
6805
 
7204
6806
 
7205
- /**
7206
- * @const {!Object.<string, string>}
7207
- * @private
7208
- */
7209
- shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_ = {
7210
- 'mp4': 'video/mp4',
7211
- 'm4v': 'video/mp4',
7212
- 'm4a': 'audio/mp4',
7213
- 'webm': 'video/webm',
7214
- 'weba': 'audio/webm',
7215
- 'mkv': 'video/webm', // Chromium browsers supports it.
7216
- 'ts': 'video/mp2t',
7217
- 'ogv': 'video/ogg',
7218
- 'ogg': 'audio/ogg',
7219
- 'mpg': 'video/mpeg',
7220
- 'mpeg': 'video/mpeg',
7221
- 'm3u8': 'application/x-mpegurl',
7222
- 'mpd': 'application/dash+xml',
7223
- 'mp3': 'audio/mpeg',
7224
- 'aac': 'audio/aac',
7225
- 'flac': 'audio/flac',
7226
- 'wav': 'audio/wav',
7227
- };
7228
-
7229
-
7230
- /**
7231
- * @const {!Object.<string, string>}
7232
- * @private
7233
- */
7234
- shaka.Player.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
7235
- 'sbv': 'text/x-subviewer',
7236
- 'srt': 'text/srt',
7237
- 'vtt': 'text/vtt',
7238
- 'webvtt': 'text/vtt',
7239
- 'ttml': 'application/ttml+xml',
7240
- 'lrc': 'application/x-subtitle-lrc',
7241
- 'ssa': 'text/x-ssa',
7242
- 'ass': 'text/x-ssa',
7243
- };
7244
-
7245
-
7246
6807
  /**
7247
6808
  * @const {string}
7248
6809
  */