shaka-player 4.5.0 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/AUTHORS +2 -0
  3. package/CHANGELOG.md +109 -0
  4. package/CODE_OF_CONDUCT.md +1 -1
  5. package/CONTRIBUTORS +2 -0
  6. package/README.md +38 -32
  7. package/demo/common/asset.js +11 -17
  8. package/demo/common/assets.js +117 -11
  9. package/demo/config.js +28 -1
  10. package/demo/custom.js +52 -38
  11. package/demo/main.js +11 -3
  12. package/demo/search.js +7 -0
  13. package/dist/controls.css +1 -1
  14. package/dist/controls.css.map +1 -1
  15. package/dist/demo.compiled.debug.js +133 -124
  16. package/dist/demo.compiled.debug.map +3 -3
  17. package/dist/demo.compiled.js +133 -124
  18. package/dist/demo.compiled.map +3 -3
  19. package/dist/deps.js +20 -18
  20. package/dist/locales.js +34 -30
  21. package/dist/receiver.compiled.debug.js +58 -50
  22. package/dist/receiver.compiled.debug.map +3 -3
  23. package/dist/receiver.compiled.js +58 -50
  24. package/dist/receiver.compiled.map +3 -3
  25. package/dist/shaka-player.compiled.d.ts +440 -390
  26. package/dist/shaka-player.compiled.debug.d.ts +440 -390
  27. package/dist/shaka-player.compiled.debug.externs.js +162 -77
  28. package/dist/shaka-player.compiled.debug.js +789 -738
  29. package/dist/shaka-player.compiled.debug.map +4 -4
  30. package/dist/shaka-player.compiled.externs.js +162 -77
  31. package/dist/shaka-player.compiled.js +1147 -1113
  32. package/dist/shaka-player.compiled.map +4 -4
  33. package/dist/shaka-player.ui.d.ts +491 -437
  34. package/dist/shaka-player.ui.debug.d.ts +491 -437
  35. package/dist/shaka-player.ui.debug.externs.js +167 -77
  36. package/dist/shaka-player.ui.debug.js +857 -791
  37. package/dist/shaka-player.ui.debug.map +4 -4
  38. package/dist/shaka-player.ui.externs.js +167 -77
  39. package/dist/shaka-player.ui.js +1359 -1314
  40. package/dist/shaka-player.ui.map +4 -4
  41. package/docs/tutorials/ad_monetization.md +54 -0
  42. package/docs/tutorials/basic-usage.md +2 -1
  43. package/docs/tutorials/errors.md +2 -1
  44. package/docs/tutorials/offline.md +10 -8
  45. package/docs/tutorials/plugins.md +5 -0
  46. package/docs/tutorials/ui-customization.md +13 -0
  47. package/docs/tutorials/ui.md +2 -1
  48. package/docs/tutorials/upgrade.md +3 -0
  49. package/externs/managedmediasource.js +24 -0
  50. package/externs/shaka/abr_manager.js +6 -2
  51. package/externs/shaka/cea.js +7 -0
  52. package/externs/shaka/codecs.js +5 -3
  53. package/externs/shaka/manifest.js +21 -5
  54. package/externs/shaka/manifest_parser.js +17 -0
  55. package/externs/shaka/net.js +2 -2
  56. package/externs/shaka/offline.js +9 -2
  57. package/externs/shaka/player.js +53 -7
  58. package/externs/shaka/text.js +3 -1
  59. package/externs/shaka/transmuxer.js +1 -1
  60. package/lib/abr/simple_abr_manager.js +25 -8
  61. package/lib/ads/media_tailor_ad_manager.js +14 -3
  62. package/lib/cast/cast_receiver.js +1 -1
  63. package/lib/cea/cea_decoder.js +28 -0
  64. package/lib/cea/dummy_caption_decoder.js +5 -0
  65. package/lib/dash/dash_parser.js +251 -84
  66. package/lib/dash/mpd_utils.js +52 -31
  67. package/lib/dash/segment_base.js +4 -5
  68. package/lib/dash/segment_list.js +5 -4
  69. package/lib/dash/segment_template.js +92 -29
  70. package/lib/hls/hls_classes.js +28 -43
  71. package/lib/hls/hls_parser.js +770 -652
  72. package/lib/hls/hls_utils.js +64 -5
  73. package/lib/hls/manifest_text_parser.js +7 -11
  74. package/lib/media/adaptation_set.js +20 -13
  75. package/lib/media/adaptation_set_criteria.js +70 -18
  76. package/lib/media/closed_caption_parser.js +13 -0
  77. package/lib/media/content_workarounds.js +6 -0
  78. package/lib/media/drm_engine.js +8 -13
  79. package/lib/media/manifest_parser.js +8 -113
  80. package/lib/media/media_source_capabilities.js +10 -3
  81. package/lib/media/media_source_engine.js +75 -21
  82. package/lib/media/play_rate_controller.js +8 -38
  83. package/lib/media/presentation_timeline.js +7 -1
  84. package/lib/media/quality_observer.js +1 -1
  85. package/lib/media/segment_index.js +5 -5
  86. package/lib/media/segment_prefetch.js +92 -24
  87. package/lib/media/segment_reference.js +67 -1
  88. package/lib/media/segment_utils.js +343 -0
  89. package/lib/media/stall_detector.js +0 -3
  90. package/lib/media/streaming_engine.js +72 -44
  91. package/lib/mss/mss_parser.js +18 -2
  92. package/lib/net/networking_engine.js +66 -12
  93. package/lib/net/networking_utils.js +95 -0
  94. package/lib/offline/indexeddb/v1_storage_cell.js +2 -0
  95. package/lib/offline/indexeddb/v2_storage_cell.js +2 -0
  96. package/lib/offline/manifest_converter.js +2 -0
  97. package/lib/offline/offline_manifest_parser.js +10 -0
  98. package/lib/offline/storage.js +11 -7
  99. package/lib/player.js +998 -1503
  100. package/lib/polyfill/mediasource.js +22 -1
  101. package/lib/text/mp4_ttml_parser.js +2 -2
  102. package/lib/text/srt_text_parser.js +2 -2
  103. package/lib/text/stub_text_displayer.js +51 -0
  104. package/lib/text/text_engine.js +3 -2
  105. package/lib/text/ttml_text_parser.js +28 -5
  106. package/lib/text/ui_text_displayer.js +4 -0
  107. package/lib/text/vtt_text_parser.js +7 -2
  108. package/lib/transmuxer/aac_transmuxer.js +1 -1
  109. package/lib/transmuxer/ac3_transmuxer.js +1 -1
  110. package/lib/transmuxer/ec3_transmuxer.js +1 -1
  111. package/lib/transmuxer/h264.js +5 -4
  112. package/lib/transmuxer/h265.js +5 -4
  113. package/lib/transmuxer/mp3_transmuxer.js +1 -1
  114. package/lib/transmuxer/mpeg_ts_transmuxer.js +1 -1
  115. package/lib/transmuxer/mss_transmuxer.js +1 -1
  116. package/lib/transmuxer/ts_transmuxer.js +33 -13
  117. package/lib/util/buffer_utils.js +26 -4
  118. package/lib/util/content_steering_manager.js +358 -0
  119. package/lib/util/error.js +2 -6
  120. package/lib/util/fairplay_utils.js +1 -1
  121. package/lib/util/fake_event.js +5 -1
  122. package/lib/util/id3_utils.js +50 -1
  123. package/lib/util/manifest_parser_utils.js +12 -8
  124. package/lib/util/mp4_generator.js +2 -4
  125. package/lib/util/mutex.js +59 -0
  126. package/lib/util/object_utils.js +1 -1
  127. package/lib/util/periods.js +154 -179
  128. package/lib/util/platform.js +4 -3
  129. package/lib/util/player_configuration.js +35 -6
  130. package/lib/util/stream_utils.js +105 -98
  131. package/lib/util/ts_parser.js +124 -73
  132. package/lib/util/uint8array_utils.js +5 -1
  133. package/lib/util/xml_utils.js +5 -0
  134. package/maintained-branches.md +2 -2
  135. package/package.json +1 -1
  136. package/roadmap.md +15 -5
  137. package/shaka-player.uncompiled.js +0 -1
  138. package/test/abr/simple_abr_manager_unit.js +33 -33
  139. package/test/ads/ad_manager_unit.js +3 -2
  140. package/test/cast/cast_receiver_integration.js +3 -4
  141. package/test/cast/cast_utils_unit.js +2 -0
  142. package/test/cea/cea_decoder_unit.js +7 -0
  143. package/test/codec_switching/codec_switching_integration.js +2 -1
  144. package/test/dash/dash_parser_integration.js +2 -1
  145. package/test/dash/dash_parser_live_unit.js +0 -19
  146. package/test/dash/dash_parser_manifest_unit.js +494 -2
  147. package/test/dash/mpd_utils_unit.js +61 -30
  148. package/test/hls/hls_live_unit.js +12 -3
  149. package/test/hls/hls_parser_unit.js +417 -27
  150. package/test/hls/manifest_text_parser_unit.js +32 -99
  151. package/test/media/adaptation_set_criteria_unit.js +172 -21
  152. package/test/media/adaptation_set_unit.js +1 -0
  153. package/test/media/content_workarounds_unit.js +1 -1
  154. package/test/media/drm_engine_unit.js +3 -3
  155. package/test/media/media_source_engine_unit.js +1 -1
  156. package/test/media/play_rate_controller_unit.js +0 -48
  157. package/test/media/segment_index_unit.js +2 -1
  158. package/test/mss/mss_player_integration.js +3 -2
  159. package/test/net/networking_engine_unit.js +3 -3
  160. package/test/{media/manifest_parser_unit.js → net/networking_utils_unit.js} +2 -2
  161. package/test/offline/manifest_convert_unit.js +9 -0
  162. package/test/offline/offline_integration.js +2 -1
  163. package/test/offline/storage_integration.js +21 -9
  164. package/test/player_external.js +4 -2
  165. package/test/player_integration.js +15 -4
  166. package/test/player_load_graph_integration.js +47 -151
  167. package/test/player_src_equals_external.js +1 -0
  168. package/test/player_src_equals_integration.js +71 -4
  169. package/test/player_unit.js +80 -27
  170. package/test/test/util/fake_media_source_engine.js +5 -0
  171. package/test/test/util/fake_networking_engine.js +9 -1
  172. package/test/test/util/fake_segment_prefetch.js +3 -0
  173. package/test/test/util/manifest_generator.js +4 -0
  174. package/test/test/util/manifest_parser_util.js +14 -2
  175. package/test/test/util/offline_utils.js +1 -0
  176. package/test/test/util/simple_fakes.js +2 -2
  177. package/test/test/util/streaming_engine_util.js +3 -0
  178. package/test/test/util/test_scheme.js +6 -0
  179. package/test/test/util/ui_utils.js +4 -3
  180. package/test/text/mp4_ttml_parser_unit.js +4 -4
  181. package/test/text/srt_text_parser_unit.js +1 -1
  182. package/test/text/text_displayer_layout_unit.js +1 -1
  183. package/test/text/text_engine_unit.js +6 -0
  184. package/test/text/ttml_text_parser_unit.js +33 -2
  185. package/test/transmuxer/transmuxer_engine_integration.js +9 -0
  186. package/test/transmuxer/transmuxer_integration.js +33 -3
  187. package/test/ui/ad_ui_unit.js +2 -2
  188. package/test/ui/ui_customization_unit.js +17 -16
  189. package/test/ui/ui_integration.js +30 -20
  190. package/test/ui/ui_unit.js +25 -19
  191. package/test/util/buffer_utils_unit.js +1 -1
  192. package/test/util/content_steering_manager_unit.js +427 -0
  193. package/test/util/id3_utils_unit.js +52 -0
  194. package/test/util/stream_utils_unit.js +17 -113
  195. package/test/util/ts_parser_unit.js +3 -3
  196. package/third_party/closure-uri/uri.js +13 -1
  197. package/ui/audio_language_selection.js +9 -2
  198. package/ui/externs/ui.js +17 -2
  199. package/ui/language_utils.js +43 -8
  200. package/ui/less/range_elements.less +33 -0
  201. package/ui/locales/en.json +1 -0
  202. package/ui/locales/es-419.json +1 -0
  203. package/ui/locales/es.json +1 -0
  204. package/ui/locales/source.json +4 -0
  205. package/ui/localization.js +10 -0
  206. package/ui/remote_button.js +3 -1
  207. package/ui/resolution_selection.js +47 -10
  208. package/ui/seek_bar.js +296 -3
  209. package/ui/text_selection.js +2 -1
  210. package/ui/ui.js +6 -2
  211. package/lib/routing/node.js +0 -23
  212. package/lib/routing/payload.js +0 -47
  213. package/lib/routing/walker.js +0 -587
  214. package/test/routing/walker_unit.js +0 -505
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');
@@ -58,8 +60,6 @@ goog.require('shaka.util.StreamUtils');
58
60
  goog.require('shaka.util.Timer');
59
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.
@@ -455,6 +443,56 @@ goog.requireType('shaka.routing.Payload');
455
443
  */
456
444
 
457
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
+
458
496
  /**
459
497
  * @summary The main player object for Shaka Player.
460
498
  *
@@ -523,6 +561,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
523
561
  /** @private {shaka.media.Playhead} */
524
562
  this.playhead_ = null;
525
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
+
526
575
  /**
527
576
  * The playhead observers are used to monitor the position of the playhead
528
577
  * and some other source of data (e.g. buffered content), and raise events.
@@ -540,12 +589,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
540
589
  */
541
590
  this.playRateController_ = null;
542
591
 
543
- // We use the buffering observer and timer to track when we move from having
544
- // enough buffered content to not enough. They only exist when content has
545
- // been loaded and are not re-used between loads.
546
- /** @private {shaka.util.Timer} */
547
- this.bufferPoller_ = null;
548
-
549
592
  /** @private {shaka.media.BufferingObserver} */
550
593
  this.bufferObserver_ = null;
551
594
 
@@ -582,6 +625,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
582
625
  /** @private {?string} */
583
626
  this.assetUri_ = null;
584
627
 
628
+ /** @private {?number} */
629
+ this.startTime_ = null;
630
+
585
631
  /** @private {boolean} */
586
632
  this.fullyLoaded_ = false;
587
633
 
@@ -601,6 +647,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
601
647
  */
602
648
  this.nextExternalStreamId_ = 1e9;
603
649
 
650
+ /** @private {!Array.<shaka.extern.Stream>} */
651
+ this.externalSrcEqualsThumbnailsStreams_ = [];
652
+
653
+ /** @private {number} */
654
+ this.completionPercent_ = NaN;
655
+
604
656
  /** @private {?shaka.extern.PlayerConfiguration} */
605
657
  this.config_ = this.defaultConfig_();
606
658
 
@@ -624,7 +676,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
624
676
  this.config_.preferredVariantRole,
625
677
  this.config_.preferredAudioChannelCount,
626
678
  this.config_.preferredVideoHdrLevel,
627
- this.config_.mediaSource.codecSwitchingStrategy);
679
+ this.config_.preferredVideoLayout,
680
+ this.config_.preferredAudioLabel,
681
+ this.config_.mediaSource.codecSwitchingStrategy,
682
+ this.config_.manifest.dash.enableAudioGroups);
628
683
 
629
684
  /** @private {string} */
630
685
  this.currentTextLanguage_ = this.config_.preferredTextLanguage;
@@ -638,15 +693,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
638
693
  /** @private {!Array.<function():(!Promise|undefined)>} */
639
694
  this.cleanupOnUnload_ = [];
640
695
 
641
- /**
642
- * This playback start position will be used when
643
- * <code>updateStartTime()</code> has been called to provide an updated
644
- * start position during the media loading process.
645
- *
646
- * @private {?number}
647
- */
648
- this.updatedStartTime_ = null;
649
-
650
696
  if (dependencyInjector) {
651
697
  dependencyInjector(this);
652
698
  }
@@ -672,117 +718,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
672
718
  this.retryStreaming();
673
719
  });
674
720
 
675
- /** @private {shaka.routing.Node} */
676
- this.detachNode_ = {name: 'detach'};
677
- /** @private {shaka.routing.Node} */
678
- this.attachNode_ = {name: 'attach'};
679
- /** @private {shaka.routing.Node} */
680
- this.unloadNode_ = {name: 'unload'};
681
- /** @private {shaka.routing.Node} */
682
- this.parserNode_ = {name: 'manifest-parser'};
683
- /** @private {shaka.routing.Node} */
684
- this.manifestNode_ = {name: 'manifest'};
685
- /** @private {shaka.routing.Node} */
686
- this.mediaSourceNode_ = {name: 'media-source'};
687
- /** @private {shaka.routing.Node} */
688
- this.drmNode_ = {name: 'drm-engine'};
689
- /** @private {shaka.routing.Node} */
690
- this.loadNode_ = {name: 'load'};
691
- /** @private {shaka.routing.Node} */
692
- this.srcEqualsDrmNode_ = {name: 'src-equals-drm-engine'};
693
- /** @private {shaka.routing.Node} */
694
- this.srcEqualsNode_ = {name: 'src-equals'};
695
-
696
- const AbortableOperation = shaka.util.AbortableOperation;
697
-
698
- const actions = new Map();
699
- actions.set(this.attachNode_, (has, wants) => {
700
- return AbortableOperation.notAbortable(this.onAttach_(has, wants));
701
- });
702
- actions.set(this.detachNode_, (has, wants) => {
703
- return AbortableOperation.notAbortable(this.onDetach_(has, wants));
704
- });
705
- actions.set(this.unloadNode_, (has, wants) => {
706
- return AbortableOperation.notAbortable(this.onUnload_(has, wants));
707
- });
708
- actions.set(this.mediaSourceNode_, (has, wants) => {
709
- const p = this.onInitializeMediaSourceEngine_(has, wants);
710
- return AbortableOperation.notAbortable(p);
711
- });
712
- actions.set(this.parserNode_, (has, wants) => {
713
- const p = this.onInitializeParser_(has, wants);
714
- return AbortableOperation.notAbortable(p);
715
- });
716
- actions.set(this.manifestNode_, (has, wants) => {
717
- // This action is actually abortable, so unlike the other callbacks, this
718
- // one will return an abortable operation.
719
- return this.onParseManifest_(has, wants);
720
- });
721
- actions.set(this.drmNode_, (has, wants) => {
722
- const p = this.onInitializeDrm_(has, wants);
723
- return AbortableOperation.notAbortable(p);
724
- });
725
- actions.set(this.loadNode_, (has, wants) => {
726
- return AbortableOperation.notAbortable(this.onLoad_(has, wants));
727
- });
728
-
729
- actions.set(this.srcEqualsDrmNode_, (has, wants) => {
730
- const p = this.onInitializeSrcEqualsDrm_(has, wants);
731
- return AbortableOperation.notAbortable(p);
732
- });
733
- actions.set(this.srcEqualsNode_, (has, wants) => {
734
- return this.onSrcEquals_(has, wants);
735
- });
736
-
737
- /** @private {shaka.routing.Walker.Implementation} */
738
- const walkerImplementation = {
739
- getNext: (at, has, goingTo, wants) => {
740
- return this.getNextStep_(at, has, goingTo, wants);
741
- },
742
- enterNode: (node, has, wants) => {
743
- this.dispatchEvent(this.makeEvent_(
744
- /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
745
- /* data= */ (new Map()).set('state', node.name)));
746
-
747
- const action = actions.get(node);
748
- return action(has, wants);
749
- },
750
- handleError: async (has, error) => {
751
- shaka.log.warning('The walker saw an error:');
752
- if (error instanceof shaka.util.Error) {
753
- shaka.log.warning('Error Code:', error.code);
754
- } else {
755
- shaka.log.warning('Error Message:', error.message);
756
- shaka.log.warning('Error Stack:', error.stack);
757
- }
758
-
759
- // Regardless of what state we were in, if there is an error, we unload.
760
- // This ensures that any initialized system will be torn-down and we
761
- // will go back to a safe foundation. We assume that the media element
762
- // is always safe to use after an error.
763
- await this.onUnload_(has, shaka.Player.createEmptyPayload_());
764
-
765
- // There are only two nodes that come before we start loading content,
766
- // attach and detach. If we have a media element, it means we were
767
- // attached to the element, and we can safely return to the attach state
768
- // (we assume that the video element is always re-usable). We favor
769
- // returning to the attach node since it means that the app won't need
770
- // to re-attach if it saw an error.
771
- return has.mediaElement ? this.attachNode_ : this.detachNode_;
772
- },
773
- onIdle: (node) => {
774
- this.dispatchEvent(this.makeEvent_(
775
- /* name= */ shaka.util.FakeEvent.EventName.OnStateIdle,
776
- /* data= */ (new Map()).set('state', node.name)));
777
- },
778
- };
779
-
780
- /** @private {shaka.routing.Walker} */
781
- this.walker_ = new shaka.routing.Walker(
782
- this.detachNode_,
783
- shaka.Player.createEmptyPayload_(),
784
- walkerImplementation);
785
-
786
721
  /** @private {shaka.util.Timer} */
787
722
  this.checkVariantsTimer_ =
788
723
  new shaka.util.Timer(() => this.checkVariants_());
@@ -791,6 +726,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
791
726
  // the LAST thing we do in the constructor because conceptually it relies on
792
727
  // player having been initialized.
793
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.');
794
733
  this.attach(mediaElement, /* initializeMediaSource= */ true);
795
734
  }
796
735
  }
@@ -833,9 +772,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
833
772
  setupLcevc_(config) {
834
773
  if (config.lcevc.enabled) {
835
774
  const tracks = this.getVariantTracks();
836
- if (tracks && tracks[0] &&
837
- tracks[0].videoMimeType ==
838
- shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_['ts']) {
775
+ if (tracks && tracks[0] && tracks[0].videoMimeType == 'video/mp2t') {
839
776
  const edge = shaka.util.Platform.isEdge() ||
840
777
  shaka.util.Platform.isLegacyEdge();
841
778
  if (edge) {
@@ -881,51 +818,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
881
818
  // If LCEVC Decoder exists close it.
882
819
  this.closeLcevcDec_();
883
820
 
821
+ const detachPromise = this.detach();
822
+
884
823
  // Mark as "dead". This should stop external-facing calls from changing our
885
824
  // internal state any more. This will stop calls to |attach|, |detach|, etc.
886
825
  // from interrupting our final move to the detached state.
887
826
  this.loadMode_ = shaka.Player.LoadMode.DESTROYED;
888
827
 
889
- const payload = shaka.Player.createEmptyPayload_();
890
- payload.keepAdManager = false;
891
-
892
- // Because we have set |loadMode_| to |DESTROYED| we can't call |detach|. We
893
- // must talk to |this.walker_| directly.
894
- const events = this.walker_.startNewRoute((currentPayload) => {
895
- return {
896
- node: this.detachNode_,
897
- payload: payload,
898
- interruptible: false,
899
- };
900
- });
901
-
902
- // Wait until the detach has finished so that we don't interrupt it by
903
- // calling |destroy| on |this.walker_|. To avoid failing here, we always
904
- // resolve the promise.
905
- await new Promise((resolve) => {
906
- events.onStart = () => {
907
- shaka.log.info('Preparing to destroy walker...');
908
- };
909
- events.onEnd = () => {
910
- resolve();
911
- };
912
- events.onCancel = () => {
913
- goog.asserts.assert(false,
914
- 'Our final detach call should never be cancelled.');
915
- resolve();
916
- };
917
- events.onError = () => {
918
- goog.asserts.assert(false,
919
- 'Our final detach call should never see an error');
920
- resolve();
921
- };
922
- events.onSkip = () => {
923
- goog.asserts.assert(false,
924
- 'Our final detach call should never be skipped');
925
- resolve();
926
- };
927
- });
928
- await this.walker_.destroy();
828
+ await detachPromise;
929
829
 
930
830
  // Tear-down the event managers to ensure handlers stop firing.
931
831
  if (this.globalEventManager_) {
@@ -1011,10 +911,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1011
911
  return false;
1012
912
  }
1013
913
 
1014
- // We do not support iOS 9, 10, 11 or 12, nor those same versions of
1015
- // desktop Safari.
1016
914
  const safariVersion = shaka.util.Platform.safariVersion();
1017
- if (safariVersion && safariVersion < 13) {
915
+ if (safariVersion && safariVersion < 9) {
1018
916
  return false;
1019
917
  }
1020
918
 
@@ -1073,56 +971,60 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1073
971
  }
1074
972
 
1075
973
  /**
1076
- * Tell the player to use <code>mediaElement</code> for all <code>load</code>
1077
- * requests until <code>detach</code> or <code>destroy</code> are called.
1078
- *
1079
- * <p>
1080
- * Calling <code>attach</code> with <code>initializedMediaSource=true</code>
1081
- * will tell the player to take the initial load step and initialize media
1082
- * source.
1083
- *
1084
- * <p>
1085
- * Calls to <code>attach</code> will interrupt any in-progress calls to
1086
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1087
- * <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.
1088
989
  *
1089
990
  * @param {!HTMLMediaElement} mediaElement
1090
991
  * @param {boolean=} initializeMediaSource
1091
992
  * @return {!Promise}
1092
993
  * @export
1093
994
  */
1094
- attach(mediaElement, initializeMediaSource = true) {
995
+ async attach(mediaElement, initializeMediaSource = true) {
1095
996
  // Do not allow the player to be used after |destroy| is called.
1096
997
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1097
- return Promise.reject(this.createAbortLoadError_());
998
+ throw this.createAbortLoadError_();
1098
999
  }
1099
1000
 
1100
- const payload = shaka.Player.createEmptyPayload_();
1101
- payload.mediaElement = mediaElement;
1102
-
1103
- // If the platform does not support media source, we will never want to
1104
- // initialize media source.
1105
- if (!shaka.util.Platform.supportsMediaSource()) {
1106
- initializeMediaSource = false;
1001
+ if (this.video_ && this.video_ != mediaElement) {
1002
+ await this.detach();
1107
1003
  }
1108
1004
 
1109
- const destination = initializeMediaSource ?
1110
- this.mediaSourceNode_ :
1111
- this.attachNode_;
1005
+ if (await this.atomicOperationAcquireMutex_('attach')) {
1006
+ return;
1007
+ }
1112
1008
 
1113
- // Do not allow this route to be interrupted because calls after this attach
1114
- // call will depend on the media element being attached.
1115
- const events = this.walker_.startNewRoute((currentPayload) => {
1116
- return {
1117
- node: destination,
1118
- payload: payload,
1119
- interruptible: false,
1120
- };
1121
- });
1009
+ try {
1010
+ this.makeStateChangeEvent_('attach');
1122
1011
 
1123
- // List to the events that can occur with our request.
1124
- events.onStart = () => shaka.log.info('Starting attach...');
1125
- 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
+ }
1126
1028
  }
1127
1029
 
1128
1030
 
@@ -1138,131 +1040,246 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1138
1040
  }
1139
1041
 
1140
1042
  /**
1141
- * Tell the player to stop using its current media element. If the player is:
1142
- * <ul>
1143
- * <li>detached, this will do nothing,
1144
- * <li>attached, this will release the media element,
1145
- * <li>loading, this will abort loading, unload, and release the media
1146
- * element,
1147
- * <li>playing content, this will stop playback, unload, and release the
1148
- * media element.
1149
- * </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.
1150
1046
  *
1151
- * <p>
1152
- * Calls to <code>detach</code> will interrupt any in-progress calls to
1153
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1154
- * <code>detach</code>, or <code>unload</code>.
1047
+ * @param {boolean=} keepAdManager
1155
1048
  *
1156
1049
  * @return {!Promise}
1157
1050
  * @export
1158
1051
  */
1159
- detach() {
1052
+ async detach(keepAdManager = false) {
1160
1053
  // Do not allow the player to be used after |destroy| is called.
1161
1054
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1162
- return Promise.reject(this.createAbortLoadError_());
1055
+ throw this.createAbortLoadError_();
1163
1056
  }
1164
1057
 
1165
- // Tell the walker to go "detached", but do not allow it to be interrupted.
1166
- // If it could be interrupted it means that our media element could fall out
1167
- // of sync.
1168
- const events = this.walker_.startNewRoute((currentPayload) => {
1169
- return {
1170
- node: this.detachNode_,
1171
- payload: shaka.Player.createEmptyPayload_(),
1172
- interruptible: false,
1173
- };
1174
- });
1058
+ await this.unload(/* initializeMediaSource= */ false, keepAdManager);
1059
+
1060
+ if (await this.atomicOperationAcquireMutex_('detach')) {
1061
+ return;
1062
+ }
1063
+
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
+ }
1175
1071
 
1176
- events.onStart = () => shaka.log.info('Starting detach...');
1177
- return this.wrapWalkerListenersWithPromise_(events);
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
+ }
1178
1081
  }
1179
1082
 
1180
1083
  /**
1181
- * Tell the player to either return to:
1182
- * <ul>
1183
- * <li>detached (when it does not have a media element),
1184
- * <li>attached (when it has a media element and
1185
- * <code>initializedMediaSource=false</code>)
1186
- * <li>media source initialized (when it has a media element and
1187
- * <code>initializedMediaSource=true</code>)
1188
- * </ul>
1189
- *
1190
- * <p>
1191
- * Calls to <code>unload</code> will interrupt any in-progress calls to
1192
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1193
- * <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.
1194
1105
  *
1195
1106
  * @param {boolean=} initializeMediaSource
1196
1107
  * @param {boolean=} keepAdManager
1197
1108
  * @return {!Promise}
1198
1109
  * @export
1199
1110
  */
1200
- unload(initializeMediaSource = true, keepAdManager = false) {
1201
- // Do not allow the player to be used after |destroy| is called.
1202
- if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1203
- 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;
1204
1118
  }
1205
1119
 
1206
- this.fullyLoaded_ = false;
1207
-
1208
- // If the platform does not support media source, we will never want to
1209
- // initialize media source.
1210
- if (!shaka.util.Platform.supportsMediaSource()) {
1211
- initializeMediaSource = false;
1120
+ if (await this.atomicOperationAcquireMutex_('unload')) {
1121
+ return;
1212
1122
  }
1213
1123
 
1214
- // If LCEVC Decoder exists close it.
1215
- this.closeLcevcDec_();
1124
+ try {
1125
+ this.fullyLoaded_ = false;
1126
+ this.makeStateChangeEvent_('unload');
1216
1127
 
1217
- // Since we are going either to attached or detached (through unloaded), we
1218
- // can't allow it to be interrupted or else we could lose track of what
1219
- // media element we are suppose to use.
1220
- //
1221
- // Using the current payload, we can determine which node we want to go to.
1222
- // If we have a media element, we want to go back to attached. If we have no
1223
- // media element, we want to go back to detached.
1224
- const payload = shaka.Player.createEmptyPayload_();
1225
- payload.keepAdManager = keepAdManager;
1226
-
1227
- const events = this.walker_.startNewRoute((currentPayload) => {
1228
- // When someone calls |unload| we can either be before attached or
1229
- // detached (there is nothing stopping someone from calling |detach| when
1230
- // we are already detached).
1231
- //
1232
- // If we are attached to the correct element, we can tear down the
1233
- // previous playback components and go to the attached media source node
1234
- // 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.
1235
1228
  //
1236
- // If we don't have a media element, we assume that we are already at the
1237
- // detached node - but only the walker knows that. To ensure we are
1238
- // actually there, we tell the walker to go to detach. While this is
1239
- // technically unnecessary, it ensures that we are in the state we want
1240
- // to be in and ready for the next request.
1241
- let destination = null;
1242
-
1243
- if (currentPayload.mediaElement && initializeMediaSource) {
1244
- destination = this.mediaSourceNode_;
1245
- } else if (currentPayload.mediaElement) {
1246
- destination = this.attachNode_;
1247
- } else {
1248
- 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();
1249
1240
  }
1250
1241
 
1251
- goog.asserts.assert(destination, 'We should have picked a destination.');
1242
+ if (this.drmEngine_) {
1243
+ await this.drmEngine_.destroy();
1244
+ this.drmEngine_ = null;
1245
+ }
1252
1246
 
1253
- // Copy over the media element because we want to keep using the same
1254
- // element - the other values don't matter.
1255
- payload.mediaElement = currentPayload.mediaElement;
1247
+ this.assetUri_ = null;
1248
+ this.bufferObserver_ = null;
1256
1249
 
1257
- return {
1258
- node: destination,
1259
- payload: payload,
1260
- interruptible: false,
1261
- };
1262
- });
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_ = [];
1263
1270
 
1264
- events.onStart = () => shaka.log.info('Starting unload...');
1265
- 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
+ }
1266
1283
  }
1267
1284
 
1268
1285
  /**
@@ -1275,120 +1292,164 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1275
1292
  * @export
1276
1293
  */
1277
1294
  updateStartTime(startTime) {
1278
- this.updatedStartTime_ = startTime;
1295
+ this.startTime_ = startTime;
1279
1296
  }
1280
1297
 
1281
1298
  /**
1282
- * Tell the player to load the content at <code>assetUri</code> and start
1283
- * playback at <code>startTime</code>. Before calling <code>load</code>,
1284
- * a call to <code>attach</code> must have succeeded.
1285
- *
1286
- * <p>
1287
- * Calls to <code>load</code> will interrupt any in-progress calls to
1288
- * <code>load</code> but cannot interrupt calls to <code>attach</code>,
1289
- * <code>detach</code>, or <code>unload</code>.
1299
+ * Loads a new stream.
1300
+ * If another stream was already playing, first unloads that stream.
1290
1301
  *
1291
1302
  * @param {string} assetUri
1292
1303
  * @param {?number=} startTime
1293
1304
  * When <code>startTime</code> is <code>null</code> or
1294
1305
  * <code>undefined</code>, playback will start at the default start time (0
1295
1306
  * for VOD and liveEdge for LIVE).
1296
- * @param {string=} mimeType
1307
+ * @param {?string=} mimeType
1297
1308
  * @return {!Promise}
1298
1309
  * @export
1299
1310
  */
1300
- load(assetUri, startTime, mimeType) {
1301
- this.updatedStartTime_ = null;
1302
-
1303
- this.fullyLoaded_ = false;
1304
-
1311
+ async load(assetUri, startTime = null, mimeType) {
1305
1312
  // Do not allow the player to be used after |destroy| is called.
1306
1313
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1307
- return Promise.reject(this.createAbortLoadError_());
1314
+ throw this.createAbortLoadError_();
1308
1315
  }
1309
1316
 
1310
- // We dispatch the loading event when someone calls |load| because we want
1311
- // to surface the user intent.
1312
- 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();
1313
1321
 
1314
- // Right away we know what the asset uri and start-of-load time are. We will
1315
- // fill-in the rest of the information later.
1316
- const payload = shaka.Player.createEmptyPayload_();
1317
- payload.uri = assetUri;
1318
- payload.startTimeOfLoad = Date.now() / 1000;
1319
- if (mimeType) {
1320
- 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);
1321
1327
  }
1322
1328
 
1323
- // Because we allow |startTime| to be optional, it means that it will be
1324
- // |undefined| when not provided. This means that we need to re-map
1325
- // |undefined| to |null| while preserving |0| as a meaningful value.
1326
- if (startTime !== undefined) {
1327
- payload.startTime = startTime;
1329
+ if (this.assetUri_) {
1330
+ await this.unload(/* initializeMediaSource= */ false);
1328
1331
  }
1329
1332
 
1330
- // TODO: Refactor to determine whether it's a manifest or not, and whether
1331
- // or not we can play it. Then we could return a better error than
1332
- // UNABLE_TO_GUESS_MANIFEST_TYPE for WebM in Safari.
1333
- const useSrcEquals = this.shouldUseSrcEquals_(payload);
1334
- 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
+ };
1335
1341
 
1336
- // Allow this request to be interrupted, this will allow other requests to
1337
- // cancel a load and quickly start a new load.
1338
- const events = this.walker_.startNewRoute((currentPayload) => {
1339
- if (currentPayload.mediaElement == null) {
1340
- // Because we return null, this "new route" will not be used.
1341
- 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();
1342
1358
  }
1359
+ };
1343
1360
 
1344
- // Keep using whatever media element we have right now.
1345
- payload.mediaElement = currentPayload.mediaElement;
1361
+ try {
1362
+ this.startTime_ = startTime;
1363
+ this.fullyLoaded_ = false;
1346
1364
 
1347
- return {
1348
- node: destination,
1349
- payload: payload,
1350
- interruptible: true,
1351
- };
1352
- });
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));
1353
1369
 
1354
- // Stats are for a single playback/load session. Stats must be initialized
1355
- // before we allow calls to |updateStateHistory|.
1356
- this.stats_ = new shaka.util.Stats();
1357
-
1358
- // Load's request is a little different, so we can't use our normal
1359
- // listeners-to-promise method. It is the only request where we may skip the
1360
- // request, so we need to set the on skip callback to reject with a specific
1361
- // error.
1362
- events.onStart =
1363
- () => shaka.log.info('Starting load of ' + assetUri + '...');
1364
- return new Promise((resolve, reject) => {
1365
- events.onSkip = () => reject(new shaka.util.Error(
1366
- shaka.util.Error.Severity.CRITICAL,
1367
- shaka.util.Error.Category.PLAYER,
1368
- shaka.util.Error.Code.NO_VIDEO_ELEMENT));
1370
+ const startTimeOfLoad = Date.now() / 1000;
1369
1371
 
1370
- events.onEnd = () => {
1371
- resolve();
1372
- // We dispatch the loaded event when the load promise is resolved
1373
- this.dispatchEvent(
1374
- this.makeEvent_(shaka.util.FakeEvent.EventName.Loaded));
1375
- };
1376
- events.onCancel = () => reject(this.createAbortLoadError_());
1377
- events.onError = (e) => reject(e);
1378
- });
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;
1379
1439
  }
1380
1440
 
1381
1441
  /**
1382
- * Check if src= should be used to load the asset at |uri|. Assume that media
1383
- * 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.
1384
1444
  *
1385
- * @param {shaka.routing.Payload} payload
1445
+ * @param {string} assetUri
1446
+ * @param {?string=} mimeType
1386
1447
  * @return {boolean}
1387
1448
  * |true| if the content should be loaded with src=, |false| if the content
1388
1449
  * should be loaded with MediaSource.
1389
1450
  * @private
1390
1451
  */
1391
- shouldUseSrcEquals_(payload) {
1452
+ shouldUseSrcEquals_(assetUri, mimeType) {
1392
1453
  const Platform = shaka.util.Platform;
1393
1454
  const MimeUtils = shaka.util.MimeUtils;
1394
1455
 
@@ -1398,29 +1459,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1398
1459
  return true;
1399
1460
  }
1400
1461
 
1401
- // The most accurate way to tell the player how to load the content is via
1402
- // MIME type. We can fall back to features of the URI if needed.
1403
- let mimeType = payload.mimeType;
1404
- const uri = payload.uri || '';
1405
-
1406
- // If we don't have a MIME type, try to guess based on the file extension.
1407
- // TODO: Too generic to belong to ManifestParser now. Refactor.
1408
- if (!mimeType) {
1409
- // Try using the uri extension.
1410
- const extension = shaka.media.ManifestParser.getExtension(uri);
1411
- mimeType = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension];
1412
- }
1413
-
1414
- // TODO: The load graph system has a design limitation that requires routing
1415
- // destination to be chosen synchronously. This means we can only make the
1416
- // right choice about src= consistently if we have a well-known file
1417
- // extension or API-provided MIME type. Detection of MIME type from a HEAD
1418
- // request (as is done for manifest types) can't be done yet.
1419
-
1420
1462
  if (mimeType) {
1421
1463
  // If we have a MIME type, check if the browser can play it natively.
1422
1464
  // This will cover both single files and native HLS.
1423
- const mediaElement = payload.mediaElement || Platform.anyMediaElement();
1465
+ const mediaElement = this.video_ || Platform.anyMediaElement();
1424
1466
  const canPlayNatively = mediaElement.canPlayType(mimeType) != '';
1425
1467
 
1426
1468
  // If we can't play natively, then src= isn't an option.
@@ -1429,7 +1471,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1429
1471
  }
1430
1472
 
1431
1473
  const canPlayMediaSource =
1432
- shaka.media.ManifestParser.isSupported(uri, mimeType);
1474
+ shaka.media.ManifestParser.isSupported(mimeType);
1433
1475
 
1434
1476
  // If MediaSource isn't an option, the native option is our only chance.
1435
1477
  if (!canPlayMediaSource) {
@@ -1443,17 +1485,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1443
1485
  // We would prefer MediaSource in some cases, and src= in others. For
1444
1486
  // example, Android has native HLS, but we'd prefer our own MediaSource
1445
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
+ }
1446
1493
 
1447
- // Native HLS can be preferred on any platform via this flag:
1448
- if (MimeUtils.isHlsType(mimeType) &&
1449
- this.config_.streaming.preferNativeHls) {
1450
- return true;
1451
- }
1452
-
1453
- // For Safari, we have an older flag which only applies to this one
1454
- // browser:
1455
- if (Platform.isApple()) {
1456
- 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
+ }
1457
1499
  }
1458
1500
 
1459
1501
  // In all other cases, we prefer MediaSource.
@@ -1467,293 +1509,35 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1467
1509
  }
1468
1510
 
1469
1511
  /**
1470
- * This should only be called by the load graph when it is time to attach to
1471
- * a media element. The only times this may be called are when we are being
1472
- * asked to re-attach to the current media element, or attach to a new media
1473
- * element while not attached to a media element.
1474
- *
1475
- * This method assumes that it is safe for it to execute, the load-graph is
1476
- * responsible for ensuring all assumptions are true.
1477
- *
1478
- * Attaching to a media element is defined as:
1479
- * - Registering error listeners to the media element.
1480
- * - Caching the video element for use outside of the load graph.
1481
- *
1482
- * @param {shaka.routing.Payload} has
1483
- * @param {shaka.routing.Payload} wants
1484
- * @return {!Promise}
1485
- * @private
1486
- */
1487
- onAttach_(has, wants) {
1488
- // If we don't have a media element yet, it means we are entering
1489
- // "attach" from another node.
1490
- //
1491
- // If we have a media element, it should match |wants.mediaElement|
1492
- // because it means we are going from "attach" to "attach".
1493
- //
1494
- // These constraints should be maintained and guaranteed by the routing
1495
- // logic in |getNextStep_|.
1496
- goog.asserts.assert(
1497
- has.mediaElement == null || has.mediaElement == wants.mediaElement,
1498
- 'The routing logic failed. MediaElement requirement failed.');
1499
-
1500
- if (has.mediaElement == null) {
1501
- has.mediaElement = wants.mediaElement;
1502
-
1503
- const onError = (error) => this.onVideoError_(error);
1504
- this.attachEventManager_.listen(has.mediaElement, 'error', onError);
1505
- }
1506
-
1507
- this.video_ = has.mediaElement;
1508
-
1509
- return Promise.resolve();
1510
- }
1511
-
1512
- /**
1513
- * This should only be called by the load graph when it is time to detach from
1514
- * a media element. The only times this may be called are when we are being
1515
- * asked to detach from the current media element, or detach when we are
1516
- * already detached.
1517
- *
1518
- * This method assumes that it is safe for it to execute, the load-graph is
1519
- * responsible for ensuring all assumptions are true.
1520
- *
1521
- * Detaching from a media element is defined as:
1522
- * - Removing error listeners from the media element.
1523
- * - Dropping the cached reference to the video element.
1524
- *
1525
- * @param {shaka.routing.Payload} has
1526
- * @param {shaka.routing.Payload} wants
1527
- * @return {!Promise}
1528
- * @private
1529
- */
1530
- onDetach_(has, wants) {
1531
- // If we were going from "detached" to "detached" we wouldn't have
1532
- // a media element to detach from.
1533
- if (has.mediaElement) {
1534
- this.attachEventManager_.removeAll();
1535
- has.mediaElement = null;
1536
- }
1537
-
1538
- if (this.adManager_ && !wants.keepAdManager) {
1539
- // The ad manager is specific to the video, so detach it too.
1540
- this.adManager_.release();
1541
- }
1542
-
1543
- // Clear our cached copy of the media element.
1544
- this.video_ = null;
1545
-
1546
- return Promise.resolve();
1547
- }
1548
-
1549
- /**
1550
- * This should only be called by the load graph when it is time to unload all
1551
- * currently initialized playback components. Unlike the other load actions,
1552
- * this action is built to be more general. We need to do this because we
1553
- * don't know what state the player will be in before unloading (including
1554
- * after an error occurred in the middle of a transition).
1555
- *
1556
- * This method assumes that any component could be |null| and should be safe
1557
- * to call from any point in the load graph.
1558
- *
1559
- * @param {shaka.routing.Payload} has
1560
- * @param {shaka.routing.Payload} wants
1561
- * @return {!Promise}
1562
- * @private
1563
- */
1564
- async onUnload_(has, wants) {
1565
- // Set the load mode to unload right away so that all the public methods
1566
- // will stop using the internal components. We need to make sure that we
1567
- // are not overriding the destroyed state because we will unload when we are
1568
- // destroying the player.
1569
- if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
1570
- this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
1571
- }
1572
-
1573
- // Run any general cleanup tasks now. This should be here at the top, right
1574
- // after setting loadMode_, so that internal components still exist as they
1575
- // did when the cleanup tasks were registered in the array.
1576
- const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb());
1577
- this.cleanupOnUnload_ = [];
1578
- await Promise.all(cleanupTasks);
1579
-
1580
- // Dispatch the unloading event.
1581
- this.dispatchEvent(
1582
- this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1583
-
1584
- // Remove everything that has to do with loading content from our payload
1585
- // since we are releasing everything that depended on it.
1586
- has.mimeType = null;
1587
- has.startTime = null;
1588
- has.uri = null;
1589
-
1590
- // Release the region timeline, which is created when parsing the manifest.
1591
- if (this.regionTimeline_) {
1592
- this.regionTimeline_.release();
1593
- this.regionTimeline_ = null;
1594
- }
1595
-
1596
- // In most cases we should have a media element. The one exception would
1597
- // be if there was an error and we, by chance, did not have a media element.
1598
- if (has.mediaElement) {
1599
- this.loadEventManager_.removeAll();
1600
- }
1601
-
1602
- // Stop the variant checker timer
1603
- this.checkVariantsTimer_.stop();
1604
-
1605
- // Some observers use some playback components, shutting down the observers
1606
- // first ensures that they don't try to use the playback components
1607
- // mid-destroy.
1608
- if (this.playheadObservers_) {
1609
- this.playheadObservers_.release();
1610
- this.playheadObservers_ = null;
1611
- }
1612
-
1613
- if (this.bufferPoller_) {
1614
- this.bufferPoller_.stop();
1615
- this.bufferPoller_ = null;
1616
- }
1617
-
1618
- // Stop the parser early. Since it is at the start of the pipeline, it
1619
- // should be start early to avoid is pushing new data downstream.
1620
- if (this.parser_) {
1621
- await this.parser_.stop();
1622
- this.parser_ = null;
1623
- this.parserFactory_ = null;
1624
- }
1625
-
1626
- // Abr Manager will tell streaming engine what to do, so we need to stop
1627
- // it before we destroy streaming engine. Unlike with the other components,
1628
- // we do not release the instance, we will reuse it in later loads.
1629
- if (this.abrManager_) {
1630
- await this.abrManager_.stop();
1631
- }
1632
-
1633
- // Streaming engine will push new data to media source engine, so we need
1634
- // to shut it down before destroy media source engine.
1635
- if (this.streamingEngine_) {
1636
- await this.streamingEngine_.destroy();
1637
- this.streamingEngine_ = null;
1638
- }
1639
-
1640
- if (this.playRateController_) {
1641
- this.playRateController_.release();
1642
- this.playRateController_ = null;
1643
- }
1644
-
1645
- // Playhead is used by StreamingEngine, so we can't destroy this until after
1646
- // StreamingEngine has stopped.
1647
- if (this.playhead_) {
1648
- this.playhead_.release();
1649
- this.playhead_ = null;
1650
- }
1651
-
1652
- // Media source engine holds onto the media element, and in order to detach
1653
- // the media keys (with drm engine), we need to break the connection between
1654
- // media source engine and the media element.
1655
- if (this.mediaSourceEngine_) {
1656
- await this.mediaSourceEngine_.destroy();
1657
- this.mediaSourceEngine_ = null;
1658
- }
1659
-
1660
- if (this.adManager_ && !wants.keepAdManager) {
1661
- this.adManager_.onAssetUnload();
1662
- }
1663
-
1664
- // In order to unload a media element, we need to remove the src attribute
1665
- // and then load again. When we destroy media source engine, this will be
1666
- // done for us, but for src=, we need to do it here.
1667
- //
1668
- // DrmEngine requires this to be done before we destroy DrmEngine itself.
1669
- if (has.mediaElement && has.mediaElement.src) {
1670
- // TODO: Investigate this more. Only reproduces on Firefox 69.
1671
- // Introduce a delay before detaching the video source. We are seeing
1672
- // spurious Promise rejections involving an AbortError in our tests
1673
- // otherwise.
1674
- await new Promise(
1675
- (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
1676
-
1677
- has.mediaElement.removeAttribute('src');
1678
- has.mediaElement.load();
1679
- // Remove all track nodes
1680
- while (has.mediaElement.lastChild) {
1681
- has.mediaElement.removeChild(has.mediaElement.firstChild);
1682
- }
1683
- }
1684
-
1685
- if (this.drmEngine_) {
1686
- await this.drmEngine_.destroy();
1687
- this.drmEngine_ = null;
1688
- }
1689
-
1690
- this.assetUri_ = null;
1691
- this.bufferObserver_ = null;
1692
-
1693
- if (this.manifest_) {
1694
- for (const variant of this.manifest_.variants) {
1695
- for (const stream of [variant.audio, variant.video]) {
1696
- if (stream && stream.segmentIndex) {
1697
- stream.segmentIndex.release();
1698
- }
1699
- }
1700
- }
1701
- for (const stream of this.manifest_.textStreams) {
1702
- if (stream.segmentIndex) {
1703
- stream.segmentIndex.release();
1704
- }
1705
- }
1706
- }
1707
-
1708
- this.manifest_ = null;
1709
- this.stats_ = new shaka.util.Stats(); // Replace with a clean stats object.
1710
- this.lastTextFactory_ = null;
1711
-
1712
- // Make sure that the app knows of the new buffering state.
1713
- this.updateBufferState_();
1714
- }
1715
-
1716
- /**
1717
- * This should only be called by the load graph when it is time to initialize
1718
- * media source engine. The only time this may be called is when we are
1719
- * attached to the same media element as in the request.
1720
- *
1721
- * This method assumes that it is safe for it to execute. The load-graph is
1722
- * responsible for ensuring all assumptions are true.
1723
- *
1724
- * @param {shaka.routing.Payload} has
1725
- * @param {shaka.routing.Payload} wants
1512
+ * Initializes the media source engine.
1726
1513
  *
1727
1514
  * @return {!Promise}
1728
1515
  * @private
1729
1516
  */
1730
- async onInitializeMediaSourceEngine_(has, wants) {
1517
+ async initializeMediaSourceEngineInner_() {
1731
1518
  goog.asserts.assert(
1732
1519
  shaka.util.Platform.supportsMediaSource(),
1733
- 'We should not be initializing media source on a platform that does ' +
1734
- 'not support media source.');
1520
+ 'We should not be initializing media source on a platform that ' +
1521
+ 'does not support media source.');
1735
1522
  goog.asserts.assert(
1736
- has.mediaElement,
1523
+ this.video_,
1737
1524
  'We should have a media element when initializing media source.');
1738
- goog.asserts.assert(
1739
- has.mediaElement == wants.mediaElement,
1740
- '|has| and |wants| should have the same media element when ' +
1741
- 'initializing media source.');
1742
-
1743
1525
  goog.asserts.assert(
1744
1526
  this.mediaSourceEngine_ == null,
1745
1527
  'We should not have a media source engine yet.');
1746
1528
 
1529
+ this.makeStateChangeEvent_('media-source');
1530
+
1747
1531
  // When changing text visibility we need to update both the text displayer
1748
- // and streaming engine because we don't always stream text. To ensure that
1749
- // text displayer and streaming engine are always in sync, wait until they
1750
- // 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.
1751
1535
  const textDisplayerFactory = this.config_.textDisplayFactory;
1752
1536
  const textDisplayer = textDisplayerFactory();
1753
1537
  this.lastTextFactory_ = textDisplayerFactory;
1754
1538
 
1755
1539
  const mediaSourceEngine = this.createMediaSourceEngine(
1756
- has.mediaElement,
1540
+ this.video_,
1757
1541
  textDisplayer,
1758
1542
  (metadata, offset, endTime) => {
1759
1543
  this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
@@ -1772,114 +1556,51 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1772
1556
  }
1773
1557
 
1774
1558
  /**
1775
- * Create the parser for the asset located at |wants.uri|. This should only be
1776
- * called as part of the load graph.
1777
- *
1778
- * This method assumes that it is safe for it to execute, the load-graph is
1779
- * responsible for ensuring all assumptions are true.
1559
+ * Pick an initialize a manifest parser, then have it download and parse the
1560
+ * manifest.
1780
1561
  *
1781
- * @param {shaka.routing.Payload} has
1782
- * @param {shaka.routing.Payload} wants
1562
+ * @param {?string=} mimeType
1783
1563
  * @return {!Promise}
1784
1564
  * @private
1785
1565
  */
1786
- async onInitializeParser_(has, wants) {
1566
+ async parseManifestInner_(mimeType) {
1787
1567
  goog.asserts.assert(
1788
- has.mediaElement,
1789
- 'We should have a media element when initializing the parser.');
1568
+ this.assetUri_,
1569
+ 'should have a uri when making the parser.');
1790
1570
  goog.asserts.assert(
1791
- has.mediaElement == wants.mediaElement,
1792
- '|has| and |wants| should have the same media element when ' +
1793
- 'initializing the parser.');
1794
-
1571
+ this.video_,
1572
+ 'We should have a media element when initializing the parser.');
1795
1573
  goog.asserts.assert(
1796
1574
  this.networkingEngine_,
1797
1575
  'Need networking engine when initializing the parser.');
1576
+ goog.asserts.assert(
1577
+ this.cmcdManager_,
1578
+ 'Need CMCD manager to populate manifest request data.');
1798
1579
  goog.asserts.assert(
1799
1580
  this.config_,
1800
1581
  'Need player config when initializing the parser.');
1801
1582
 
1802
- // We are going to "lock-in" the mime type and uri since they are
1803
- // what we are going to use to create our parser and parse the manifest.
1804
- has.mimeType = wants.mimeType;
1805
- has.uri = wants.uri;
1806
-
1807
- goog.asserts.assert(
1808
- has.uri,
1809
- 'We should have an asset uri when initializing the parsing.');
1810
-
1811
- // Store references to things we asserted so that we don't need to reassert
1812
- // them again later.
1813
- const assetUri = has.uri;
1583
+ // Store references to things we asserted so that we don't need to
1584
+ // reassert them again later.
1814
1585
  const networkingEngine = this.networkingEngine_;
1815
1586
 
1816
- // Save the uri so that it can be used outside of the load-graph.
1817
- this.assetUri_ = assetUri;
1587
+ this.makeStateChangeEvent_('manifest-parser');
1818
1588
 
1819
1589
  // Create the parser that we will use to parse the manifest.
1820
- this.parserFactory_ = await shaka.media.ManifestParser.getFactory(
1821
- assetUri,
1822
- networkingEngine,
1823
- this.config_.manifest.retryParameters,
1824
- has.mimeType);
1590
+ this.parserFactory_ = shaka.media.ManifestParser.getFactory(
1591
+ this.assetUri_,
1592
+ mimeType || null);
1825
1593
  goog.asserts.assert(this.parserFactory_, 'Must have manifest parser');
1826
1594
  this.parser_ = this.parserFactory_();
1827
1595
 
1828
1596
  const manifestConfig =
1829
1597
  shaka.util.ObjectUtils.cloneObject(this.config_.manifest);
1830
1598
  // Don't read video segments if the player is attached to an audio element
1831
- if (wants.mediaElement && wants.mediaElement.nodeName === 'AUDIO') {
1599
+ if (this.video_ && this.video_.nodeName === 'AUDIO') {
1832
1600
  manifestConfig.disableVideo = true;
1833
1601
  }
1834
1602
 
1835
1603
  this.parser_.configure(manifestConfig);
1836
- }
1837
-
1838
- /**
1839
- * Parse the manifest at |has.uri| using the parser that should have already
1840
- * been created. This should only be called as part of the load graph.
1841
- *
1842
- * This method assumes that it is safe for it to execute, the load-graph is
1843
- * responsible for ensuring all assumptions are true.
1844
- *
1845
- * @param {shaka.routing.Payload} has
1846
- * @param {shaka.routing.Payload} wants
1847
- * @return {!shaka.util.AbortableOperation}
1848
- * @private
1849
- */
1850
- onParseManifest_(has, wants) {
1851
- goog.asserts.assert(
1852
- has.mimeType == wants.mimeType,
1853
- '|has| and |wants| should have the same mime type when parsing.');
1854
- goog.asserts.assert(
1855
- has.uri == wants.uri,
1856
- '|has| and |wants| should have the same uri when parsing.');
1857
-
1858
- goog.asserts.assert(
1859
- has.uri,
1860
- '|has| should have a valid uri when parsing.');
1861
- goog.asserts.assert(
1862
- has.uri == this.assetUri_,
1863
- '|has.uri| should match the cached asset uri.');
1864
-
1865
- goog.asserts.assert(
1866
- this.networkingEngine_,
1867
- 'Need networking engine to parse manifest.');
1868
- goog.asserts.assert(
1869
- this.cmcdManager_,
1870
- 'Need CMCD manager to populate manifest request data.');
1871
- goog.asserts.assert(
1872
- this.config_,
1873
- 'Need player config to parse manifest.');
1874
-
1875
- goog.asserts.assert(
1876
- this.parser_,
1877
- '|this.parser_| should have been set in an earlier step.');
1878
-
1879
- // Store references to things we asserted so that we don't need to reassert
1880
- // them again later.
1881
- const assetUri = has.uri;
1882
- const networkingEngine = this.networkingEngine_;
1883
1604
 
1884
1605
  // This will be needed by the parser once it starts parsing, so we will
1885
1606
  // initialize it now even through it appears a little out-of-place.
@@ -1920,7 +1641,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1920
1641
  // Called when the parser finds a timeline region. This can be called
1921
1642
  // before we start playback or during playback (live/in-progress
1922
1643
  // manifest).
1923
- onTimelineRegionAdded: (region) => this.regionTimeline_.addRegion(region),
1644
+ onTimelineRegionAdded: (region) => {
1645
+ this.regionTimeline_.addRegion(region);
1646
+ },
1924
1647
 
1925
1648
  onEvent: (event) => this.dispatchEvent(event),
1926
1649
  onError: (error) => this.onError_(error),
@@ -1956,57 +1679,41 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1956
1679
 
1957
1680
  const startTime = Date.now() / 1000;
1958
1681
 
1959
- return new shaka.util.AbortableOperation(/* promise= */ (async () => {
1960
- this.manifest_ = await this.parser_.start(assetUri, playerInterface);
1682
+ this.makeStateChangeEvent_('manifest');
1961
1683
 
1962
- // This event is fired after the manifest is parsed, but before any
1963
- // filtering takes place.
1964
- const event =
1965
- this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
1966
- this.dispatchEvent(event);
1684
+ this.manifest_ = await this.parser_.start(
1685
+ this.assetUri_, playerInterface);
1967
1686
 
1968
- // We require all manifests to have at least one variant.
1969
- if (this.manifest_.variants.length == 0) {
1970
- throw new shaka.util.Error(
1971
- shaka.util.Error.Severity.CRITICAL,
1972
- shaka.util.Error.Category.MANIFEST,
1973
- shaka.util.Error.Code.NO_VARIANTS);
1974
- }
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);
1975
1692
 
1976
- // Make sure that all variants are either: audio-only, video-only, or
1977
- // audio-video.
1978
- 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
+ }
1979
1700
 
1980
- const now = Date.now() / 1000;
1981
- const delta = now - startTime;
1982
- this.stats_.setManifestTime(delta);
1983
- })(), /* onAbort= */ () => {
1984
- shaka.log.info('Aborting parser step...');
1985
- return this.parser_.stop();
1986
- });
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);
1987
1708
  }
1988
1709
 
1989
1710
  /**
1990
- * This should only be called by the load graph when it is time to initialize
1991
- * drmEngine. The only time this may be called is when we are attached a
1992
- * media element and have parsed a manifest.
1993
- *
1994
- * The load-graph is responsible for ensuring all assumptions made by this
1995
- * method are valid before executing it.
1711
+ * Initializes the DRM engine.
1996
1712
  *
1997
- * @param {shaka.routing.Payload} has
1998
- * @param {shaka.routing.Payload} wants
1999
1713
  * @return {!Promise}
2000
1714
  * @private
2001
1715
  */
2002
- async onInitializeDrm_(has, wants) {
2003
- goog.asserts.assert(
2004
- has.mimeType == wants.mimeType,
2005
- 'The load graph should have ensured the mime types matched.');
2006
- goog.asserts.assert(
2007
- has.uri == wants.uri,
2008
- 'The load graph should have ensured the uris matched');
2009
-
1716
+ async initializeDrmInner_() {
2010
1717
  goog.asserts.assert(
2011
1718
  this.networkingEngine_,
2012
1719
  '|onInitializeDrm_| should never be called after |destroy|');
@@ -2017,9 +1724,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2017
1724
  this.manifest_,
2018
1725
  '|this.manifest_| should have been set in an earlier step.');
2019
1726
  goog.asserts.assert(
2020
- has.mediaElement,
1727
+ this.video_,
2021
1728
  'We should have a media element when initializing the DRM Engine.');
2022
1729
 
1730
+ this.makeStateChangeEvent_('drm-engine');
1731
+
2023
1732
  const startTime = Date.now() / 1000;
2024
1733
  let firstEvent = true;
2025
1734
 
@@ -2042,12 +1751,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2042
1751
  const now = Date.now() / 1000;
2043
1752
  const delta = now - startTime;
2044
1753
  this.stats_.setDrmTime(delta);
2045
- // LCEVC data by itself is not encrypted in DRM protected streams and
2046
- // can therefore be accessed and decoded as normal. However, the LCEVC
2047
- // decoder needs access to the VideoElement output in order to apply
2048
- // the enhancement. In DRM contexts where the browser CDM restricts
2049
- // access from our decoder, the enhancement cannot be applied and
2050
- // therefore the LCEVC output canvas is hidden accordingly.
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.
2051
1761
  if (this.lcevcDec_) {
2052
1762
  this.lcevcDec_.hideCanvas();
2053
1763
  }
@@ -2061,66 +1771,31 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2061
1771
  this.manifest_.variants,
2062
1772
  this.manifest_.offlineSessionIds);
2063
1773
 
2064
- await this.drmEngine_.attach(has.mediaElement);
1774
+ await this.drmEngine_.attach(this.video_);
2065
1775
 
2066
- // Now that we have drm information, filter the manifest (again) so that we
2067
- // 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.
2068
1778
  await this.filterManifest_(this.manifest_);
2069
1779
  }
2070
1780
 
2071
1781
  /**
2072
- * This should only be called by the load graph when it is time to load all
2073
- * playback components needed for playback. The only times this may be called
2074
- * is when we are attached to the same media element as in the request.
1782
+ * Starts loading the content described by the parsed manifest.
2075
1783
  *
2076
- * This method assumes that it is safe for it to execute, the load-graph is
2077
- * responsible for ensuring all assumptions are true.
2078
- *
2079
- * Loading is defined as:
2080
- * - Attaching all playback-related listeners to the media element
2081
- * - Initializing playback and observers
2082
- * - Initializing ABR Manager
2083
- * - Initializing Streaming Engine
2084
- * - Starting playback at |wants.startTime|
2085
- *
2086
- * @param {shaka.routing.Payload} has
2087
- * @param {shaka.routing.Payload} wants
1784
+ * @param {number} startTimeOfLoad
1785
+ * @return {!Promise}
2088
1786
  * @private
2089
1787
  */
2090
- async onLoad_(has, wants) {
2091
- goog.asserts.assert(
2092
- has.mimeType == wants.mimeType,
2093
- '|has| and |wants| should have the same mime type when loading.');
1788
+ async loadInner_(startTimeOfLoad) {
2094
1789
  goog.asserts.assert(
2095
- has.uri == wants.uri,
2096
- '|has| and |wants| should have the same uri when loading.');
2097
-
1790
+ this.video_, 'We should have a media element by now.');
2098
1791
  goog.asserts.assert(
2099
- has.mediaElement,
2100
- 'We should have a media element when loading.');
1792
+ this.manifest_, 'The manifest should already be parsed.');
2101
1793
  goog.asserts.assert(
2102
- !isNaN(wants.startTimeOfLoad),
2103
- '|wants| should tell us when the load was originally requested');
2104
-
2105
- // Since we are about to start playback, we will lock in the start time as
2106
- // something we are now depending on.
2107
- has.startTime = wants.startTime;
2108
-
2109
- // If updateStartTime() has been called since load() was invoked use the
2110
- // requested startTime
2111
- if (this.updatedStartTime_ != null) {
2112
- has.startTime = this.updatedStartTime_;
2113
- this.updatedStartTime_ = null;
2114
- }
2115
-
2116
- // Store a reference to values in |has| after asserting so that closure will
2117
- // know that they will still be non-null between calls to await.
2118
- const mediaElement = has.mediaElement;
2119
- const assetUri = has.uri;
1794
+ this.assetUri_, 'We should have an asset uri by now.');
2120
1795
 
2121
- // Save the uri so that it can be used outside of the load-graph.
2122
- this.assetUri_ = assetUri;
1796
+ this.makeStateChangeEvent_('load');
2123
1797
 
1798
+ const mediaElement = this.video_;
2124
1799
  this.playRateController_ = new shaka.media.PlayRateController({
2125
1800
  getRate: () => mediaElement.playbackRate,
2126
1801
  getDefaultRate: () => mediaElement.defaultPlaybackRate,
@@ -2130,13 +1805,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2130
1805
 
2131
1806
  const updateStateHistory = () => this.updateStateHistory_();
2132
1807
  const onRateChange = () => this.onRateChange_();
2133
- this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory);
1808
+ this.loadEventManager_.listen(
1809
+ mediaElement, 'playing', updateStateHistory);
2134
1810
  this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
2135
1811
  this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
2136
1812
  this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
2137
1813
 
2138
- // Check status of the LCEVC Dec Object and reset or
2139
- // create or close based on config
1814
+ // Check the status of the LCEVC Dec Object. Reset, create, or close
1815
+ // depending on the config.
2140
1816
  this.setupLcevc_(this.config_);
2141
1817
 
2142
1818
  const abrFactory = this.config_.abrFactory;
@@ -2160,8 +1836,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2160
1836
  this.config_.preferredVariantRole,
2161
1837
  this.config_.preferredAudioChannelCount,
2162
1838
  this.config_.preferredVideoHdrLevel,
1839
+ this.config_.preferredVideoLayout,
2163
1840
  this.config_.preferredAudioLabel,
2164
- this.config_.mediaSource.codecSwitchingStrategy);
1841
+ this.config_.mediaSource.codecSwitchingStrategy,
1842
+ this.config_.manifest.dash.enableAudioGroups);
2165
1843
 
2166
1844
  this.currentTextLanguage_ = this.config_.preferredTextLanguage;
2167
1845
  this.currentTextRole_ = this.config_.preferredTextRole;
@@ -2182,7 +1860,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2182
1860
  this.manifest_,
2183
1861
  this.config_.preferredVideoCodecs,
2184
1862
  this.config_.preferredAudioCodecs,
2185
- this.config_.preferredAudioChannelCount,
2186
1863
  this.config_.preferredDecodingAttributes);
2187
1864
 
2188
1865
  this.streamingEngine_ = this.createStreamingEngine();
@@ -2224,7 +1901,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2224
1901
  let initialVariant = null;
2225
1902
  const activeVariant = this.streamingEngine_.getCurrentVariant();
2226
1903
  if (!activeVariant) {
2227
- initialVariant = this.chooseVariant_();
1904
+ initialVariant = this.chooseVariant_(/* initialSelection= */ true);
2228
1905
  goog.asserts.assert(initialVariant, 'Must choose an initial variant!');
2229
1906
  }
2230
1907
 
@@ -2240,20 +1917,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2240
1917
  await Promise.all(createSegmentIndexPromises);
2241
1918
  }
2242
1919
 
1920
+ if (this.parser_ && this.parser_.onInitialVariantChosen) {
1921
+ this.parser_.onInitialVariantChosen(toLazyLoad);
1922
+ }
1923
+
2243
1924
  shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
2244
1925
  this.config_.playRangeStart,
2245
1926
  this.config_.playRangeEnd);
2246
1927
 
2247
- this.playhead_ = this.createPlayhead(has.startTime);
1928
+ this.playhead_ = this.createPlayhead(this.startTime_);
2248
1929
  this.playheadObservers_ =
2249
- this.createPlayheadObserversForMSE_(has.startTimeOfLoad);
1930
+ this.createPlayheadObserversForMSE_(startTimeOfLoad);
2250
1931
 
2251
- // We need to start the buffer management code near the end because it will
2252
- // set the initial buffering state and that depends on other components
2253
- // 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.
2254
1935
  const rebufferThreshold = Math.max(
2255
1936
  this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal);
2256
- this.startBufferManagement_(rebufferThreshold);
1937
+ this.startBufferManagement_(mediaElement, rebufferThreshold);
2257
1938
 
2258
1939
  // Now we can switch to the initial variant.
2259
1940
  if (!activeVariant) {
@@ -2263,14 +1944,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2263
1944
  this.switchVariant_(initialVariant, /* fromAdaptation= */ true,
2264
1945
  /* clearBuffer= */ false, /* safeMargin= */ 0);
2265
1946
 
2266
- // Now that we have initial streams, we may adjust the start time to align
2267
- // to a segment boundary.
1947
+ // Now that we have initial streams, we may adjust the start time to
1948
+ // align to a segment boundary.
2268
1949
  if (this.config_.streaming.startAtSegmentBoundary) {
2269
- const startTime = this.playhead_.getTime();
2270
- const adjustedTime =
2271
- await this.adjustStartTime_(initialVariant, startTime);
2272
-
2273
- this.playhead_.setStartTime(adjustedTime);
1950
+ this.playhead_.setStartTime(await this.adjustStartTime_(
1951
+ initialVariant, this.playhead_.getTime()));
2274
1952
  }
2275
1953
  }
2276
1954
 
@@ -2293,7 +1971,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2293
1971
  this.setInitialTextState_(initialVariant, initialTextStream);
2294
1972
  }
2295
1973
 
2296
- // 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.
2297
1976
  if (initialTextStream && this.shouldStreamText_()) {
2298
1977
  this.streamingEngine_.switchTextStream(initialTextStream);
2299
1978
  }
@@ -2309,7 +1988,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2309
1988
  this.onAbrStatusChanged_();
2310
1989
  }
2311
1990
 
2312
- // Dispatch a 'trackschanged' event now that all initial filtering is done.
1991
+ // Dispatch a 'trackschanged' event now that all initial filtering is
1992
+ // done.
2313
1993
  this.onTracksChanged_();
2314
1994
 
2315
1995
  // Now that we've filtered out variants that aren't compatible with the
@@ -2323,8 +2003,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2323
2003
 
2324
2004
  const hasPrimary = this.manifest_.variants.some((v) => v.primary);
2325
2005
  if (!this.config_.preferredAudioLanguage && !hasPrimary) {
2326
- shaka.log.warning('No preferred audio language set. We have chosen an ' +
2327
- 'arbitrary language initially');
2006
+ shaka.log.warning('No preferred audio language set. ' +
2007
+ 'We have chosen an arbitrary language initially');
2328
2008
  }
2329
2009
 
2330
2010
  const isLive = this.isLive();
@@ -2333,6 +2013,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2333
2013
  this.manifest_.serviceDescription)) {
2334
2014
  const onTimeUpdate = () => this.onTimeUpdate_();
2335
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_();
2336
2021
  }
2337
2022
 
2338
2023
  if (this.adManager_) {
@@ -2344,24 +2029,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2344
2029
  // Wait for the 'loadedmetadata' event to measure load() latency.
2345
2030
  this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2346
2031
  const now = Date.now() / 1000;
2347
- const delta = now - wants.startTimeOfLoad;
2032
+ const delta = now - startTimeOfLoad;
2348
2033
  this.stats_.setLoadLatency(delta);
2349
2034
  });
2350
2035
  }
2351
2036
 
2352
2037
  /**
2353
- * This should only be called by the load graph when it is time to initialize
2354
- * drmEngine for src= playbacks.
2355
- *
2356
- * The load-graph is responsible for ensuring all assumptions made by this
2357
- * method are valid before executing it.
2038
+ * Initializes the DRM engine for use by src equals.
2358
2039
  *
2359
- * @param {shaka.routing.Payload} has
2360
- * @param {shaka.routing.Payload} wants
2040
+ * @param {string} mimeType
2361
2041
  * @return {!Promise}
2362
2042
  * @private
2363
2043
  */
2364
- async onInitializeSrcEqualsDrm_(has, wants) {
2044
+ async initializeSrcEqualsDrmInner_(mimeType) {
2365
2045
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
2366
2046
 
2367
2047
  goog.asserts.assert(
@@ -2399,20 +2079,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2399
2079
 
2400
2080
  this.drmEngine_.configure(this.config_.drm);
2401
2081
 
2402
- const uri = wants.uri || '';
2403
- const extension = shaka.media.ManifestParser.getExtension(uri);
2404
- let mimeType = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension];
2405
- if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) {
2406
- mimeType = 'application/vnd.apple.mpegurl';
2407
- }
2408
- if (!mimeType) {
2409
- mimeType = 'video/mp4';
2410
- }
2411
-
2412
2082
  // TODO: Instead of feeding DrmEngine with Variants, we should refactor
2413
2083
  // DrmEngine so that it takes a minimal config derived from Variants. In
2414
2084
  // cases like this one or in removal of stored content, the details are
2415
- // largely unimportant. We should have a saner way to initialize DrmEngine.
2085
+ // largely unimportant. We should have a saner way to initialize
2086
+ // DrmEngine.
2416
2087
  // That would also insulate DrmEngine from manifest changes in the future.
2417
2088
  // For now, that is time-consuming and this synthetic Variant is easy, so
2418
2089
  // I'm putting it off. Since this is only expected to be used for native
@@ -2430,10 +2101,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2430
2101
  groupId: null,
2431
2102
  createSegmentIndex: () => Promise.resolve(),
2432
2103
  segmentIndex: null,
2433
- mimeType: wants.mimeType ?
2434
- shaka.util.MimeUtils.getBasicType(wants.mimeType) : mimeType,
2435
- codecs: wants.mimeType ?
2436
- shaka.util.MimeUtils.getCodecs(wants.mimeType) : '',
2104
+ mimeType: mimeType ? shaka.util.MimeUtils.getBasicType(mimeType) : '',
2105
+ codecs: mimeType ? shaka.util.MimeUtils.getCodecs(mimeType) : '',
2437
2106
  encrypted: true,
2438
2107
  drmInfos: [], // Filled in by DrmEngine config.
2439
2108
  keyIds: new Set(),
@@ -2452,6 +2121,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2452
2121
  closedCaptions: null,
2453
2122
  accessibilityPurpose: null,
2454
2123
  external: false,
2124
+ fastSwitching: false,
2455
2125
  },
2456
2126
  bandwidth: 100,
2457
2127
  allowedByApplication: true,
@@ -2462,46 +2132,28 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2462
2132
  this.drmEngine_.setSrcEquals(/* srcEquals= */ true);
2463
2133
  await this.drmEngine_.initForPlayback(
2464
2134
  [variant], /* offlineSessionIds= */ []);
2465
- await this.drmEngine_.attach(has.mediaElement);
2135
+ await this.drmEngine_.attach(this.video_);
2466
2136
  }
2467
2137
 
2468
2138
  /**
2469
- * This should only be called by the load graph when it is time to set-up the
2470
- * media element to play content using src=. The only times this may be called
2471
- * is when we are attached to the same media element as in the request.
2139
+ * Passes the asset URI along to the media element, so it can be played src
2140
+ * equals style.
2472
2141
  *
2473
- * This method assumes that it is safe for it to execute, the load-graph is
2474
- * responsible for ensuring all assumptions are true.
2475
- *
2476
- * @param {shaka.routing.Payload} has
2477
- * @param {shaka.routing.Payload} wants
2478
- * @return {!shaka.util.AbortableOperation}
2142
+ * @param {number} startTimeOfLoad
2143
+ * @param {string} mimeType
2144
+ * @return {!Promise}
2479
2145
  *
2480
2146
  * @private
2481
2147
  */
2482
- onSrcEquals_(has, wants) {
2483
- goog.asserts.assert(
2484
- has.mediaElement,
2485
- 'We should have a media element when loading.');
2486
- goog.asserts.assert(
2487
- wants.uri,
2488
- '|has| should have a valid uri when loading.');
2148
+ async srcEqualsInner_(startTimeOfLoad, mimeType) {
2149
+ this.makeStateChangeEvent_('src-equals');
2150
+
2489
2151
  goog.asserts.assert(
2490
- !isNaN(wants.startTimeOfLoad),
2491
- '|wants| should tell us when the load was originally requested');
2152
+ this.video_, 'We should have a media element when loading.');
2492
2153
  goog.asserts.assert(
2493
- this.video_ == has.mediaElement,
2494
- 'The video element should match our media element');
2495
-
2496
- // Lock-in the values that we are using so that the routing logic knows what
2497
- // we have.
2498
- has.uri = wants.uri;
2499
- has.startTime = wants.startTime;
2154
+ this.assetUri_, 'We should have a valid uri when loading.');
2500
2155
 
2501
- // Save the uri so that it can be used outside of the load-graph.
2502
- this.assetUri_ = has.uri;
2503
-
2504
- const mediaElement = has.mediaElement;
2156
+ const mediaElement = this.video_;
2505
2157
 
2506
2158
  this.playhead_ = new shaka.media.SrcEqualsPlayhead(mediaElement);
2507
2159
 
@@ -2512,8 +2164,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2512
2164
  unloaded = true;
2513
2165
  });
2514
2166
 
2515
- if (has.startTime != null) {
2516
- this.playhead_.setStartTime(has.startTime);
2167
+ if (this.startTime_ != null) {
2168
+ this.playhead_.setStartTime(this.startTime_);
2517
2169
  }
2518
2170
 
2519
2171
  this.playRateController_ = new shaka.media.PlayRateController({
@@ -2523,16 +2175,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2523
2175
  movePlayhead: (delta) => { mediaElement.currentTime += delta; },
2524
2176
  });
2525
2177
 
2526
- // We need to start the buffer management code near the end because it will
2527
- // set the initial buffering state and that depends on other components
2528
- // 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.
2529
2181
  const rebufferThreshold = this.config_.streaming.rebufferingGoal;
2530
- this.startBufferManagement_(rebufferThreshold);
2182
+ this.startBufferManagement_(mediaElement, rebufferThreshold);
2531
2183
 
2532
2184
  // Add all media element listeners.
2533
2185
  const updateStateHistory = () => this.updateStateHistory_();
2534
2186
  const onRateChange = () => this.onRateChange_();
2535
- this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory);
2187
+ this.loadEventManager_.listen(
2188
+ mediaElement, 'playing', updateStateHistory);
2536
2189
  this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
2537
2190
  this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
2538
2191
  this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
@@ -2542,24 +2195,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2542
2195
  // automatically.
2543
2196
  // See https://github.com/shaka-project/shaka-player/issues/2483
2544
2197
  if (mediaElement.preload != 'none') {
2545
- this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2546
- const now = Date.now() / 1000;
2547
- const delta = now - wants.startTimeOfLoad;
2548
- this.stats_.setLoadLatency(delta);
2549
- });
2198
+ this.loadEventManager_.listenOnce(
2199
+ mediaElement, 'loadedmetadata', () => {
2200
+ const now = Date.now() / 1000;
2201
+ const delta = now - startTimeOfLoad;
2202
+ this.stats_.setLoadLatency(delta);
2203
+ });
2550
2204
  }
2551
2205
 
2552
2206
  // The audio tracks are only available on Safari at the moment, but this
2553
2207
  // drives the tracks API for Safari's native HLS. So when they change,
2554
2208
  // fire the corresponding Shaka Player event.
2555
2209
  if (mediaElement.audioTracks) {
2556
- this.loadEventManager_.listen(
2557
- mediaElement.audioTracks, 'addtrack', () => this.onTracksChanged_());
2558
- this.loadEventManager_.listen(
2559
- 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',
2560
2215
  () => this.onTracksChanged_());
2561
- this.loadEventManager_.listen(
2562
- mediaElement.audioTracks, 'change', () => this.onTracksChanged_());
2563
2216
  }
2564
2217
 
2565
2218
  if (mediaElement.textTracks) {
@@ -2595,13 +2248,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2595
2248
  () => this.onTracksChanged_());
2596
2249
  }
2597
2250
 
2598
- const extension = shaka.media.ManifestParser.getExtension(has.uri);
2599
- const mimeType =
2600
- shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension];
2601
-
2602
2251
  // By setting |src| we are done "loading" with src=. We don't need to set
2603
2252
  // the current time because |playhead| will do that for us.
2604
- mediaElement.src = this.cmcdManager_.appendSrcData(has.uri, mimeType);
2253
+ mediaElement.src = this.cmcdManager_.appendSrcData(
2254
+ this.assetUri_, mimeType);
2605
2255
 
2606
2256
  // Tizen 3 / WebOS won't load anything unless you call load() explicitly,
2607
2257
  // no matter the value of the preload attribute. This is harmful on some
@@ -2615,15 +2265,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2615
2265
  // initialized.
2616
2266
  this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS;
2617
2267
 
2618
- // The event doesn't mean as much for src= playback, since we don't control
2619
- // streaming. But we should fire it in this path anyway since some
2620
- // 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.
2621
2271
  this.dispatchEvent(
2622
2272
  this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
2623
2273
 
2624
2274
  // The "load" Promise is resolved when we have loaded the metadata. If we
2625
- // wait for the full data, that won't happen on Safari until the play button
2626
- // is hit.
2275
+ // wait for the full data, that won't happen on Safari until the play
2276
+ // button is hit.
2627
2277
  const fullyLoaded = new shaka.util.PublicPromise();
2628
2278
  shaka.util.MediaReadyState.waitForReadyState(mediaElement,
2629
2279
  HTMLMediaElement.HAVE_METADATA,
@@ -2633,7 +2283,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2633
2283
  fullyLoaded.resolve();
2634
2284
  });
2635
2285
 
2636
- // 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.
2637
2288
  shaka.util.MediaReadyState.waitForReadyState(mediaElement,
2638
2289
  HTMLMediaElement.HAVE_CURRENT_DATA,
2639
2290
  this.loadEventManager_,
@@ -2674,9 +2325,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2674
2325
  } else if (mediaElement.preload == 'none') {
2675
2326
  shaka.log.alwaysWarn(
2676
2327
  'With <video preload="none">, the browser will not load anything ' +
2677
- 'until play() is called. We are unable to measure load latency in ' +
2678
- 'a meaningful way, and we cannot provide track info yet. Please do ' +
2679
- '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.');
2680
2331
  // We can't wait for an event load loadedmetadata, since that will be
2681
2332
  // blocked until a user interaction. So resolve the Promise now.
2682
2333
  fullyLoaded.resolve();
@@ -2686,38 +2337,37 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2686
2337
  fullyLoaded.reject(this.videoErrorToShakaError_());
2687
2338
  });
2688
2339
 
2689
- return new shaka.util.AbortableOperation(fullyLoaded, /* onAbort= */ () => {
2690
- const abortedError = new shaka.util.Error(
2691
- shaka.util.Error.Severity.CRITICAL,
2692
- shaka.util.Error.Category.PLAYER,
2693
- shaka.util.Error.Code.OPERATION_ABORTED);
2694
- fullyLoaded.reject(abortedError);
2695
- return Promise.resolve(); // Abort complete.
2696
- }).chain(() => {
2697
- if (this.isLive() && this.config_.streaming.liveSync) {
2698
- const onTimeUpdate = () => this.onTimeUpdate_();
2699
- this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
2700
- }
2340
+ await fullyLoaded;
2701
2341
 
2702
- if (this.adManager_) {
2703
- const isLive = this.isLive();
2704
- this.adManager_.onManifestUpdated(isLive);
2705
- // There is no good way to detect when the manifest has been updated,
2706
- // so we use seekRange().end so we can tell when it has been updated.
2707
- if (isLive) {
2708
- let prevSeekRangeEnd = this.seekRange().end;
2709
- this.loadEventManager_.listen(mediaElement, 'progress', () => {
2710
- const newSeekRangeEnd = this.seekRange().end;
2711
- if (prevSeekRangeEnd != newSeekRangeEnd) {
2712
- this.adManager_.onManifestUpdated(this.isLive());
2713
- prevSeekRangeEnd = newSeekRangeEnd;
2714
- }
2715
- });
2716
- }
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
+ });
2717
2367
  }
2368
+ }
2718
2369
 
2719
- this.fullyLoaded_ = true;
2720
- });
2370
+ this.fullyLoaded_ = true;
2721
2371
  }
2722
2372
 
2723
2373
  /**
@@ -2910,6 +2560,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2910
2560
  }
2911
2561
  }
2912
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
+
2913
2571
  /**
2914
2572
  * Create a new DrmEngine instance. This may be replaced by tests to create
2915
2573
  * fake instances. Configuration and initialization will be handled after
@@ -2930,13 +2588,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2930
2588
  * @return {!shaka.net.NetworkingEngine}
2931
2589
  */
2932
2590
  createNetworkingEngine() {
2933
- /** @type {function(number, number)} */
2934
- const onProgressUpdated_ = (deltaTimeMs, bytesDownloaded) => {
2591
+ /** @type {function(number, number, boolean)} */
2592
+ const onProgressUpdated_ = (deltaTimeMs, bytesDownloaded, allowSwitch) => {
2935
2593
  // In some situations, such as during offline storage, the abr manager
2936
2594
  // might not yet exist. Therefore, we need to check if abr manager has
2937
2595
  // been initialized before using it.
2938
2596
  if (this.abrManager_) {
2939
- this.abrManager_.segmentDownloaded(deltaTimeMs, bytesDownloaded);
2597
+ this.abrManager_.segmentDownloaded(
2598
+ deltaTimeMs, bytesDownloaded, allowSwitch);
2940
2599
  }
2941
2600
  };
2942
2601
  /** @type {shaka.net.NetworkingEngine.OnHeadersReceived} */
@@ -2965,8 +2624,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2965
2624
  this.cmcdManager_.applyData(type, request, context);
2966
2625
  };
2967
2626
 
2627
+
2628
+ const onRetry_ = (type, context, newUrl, oldUrl) => {
2629
+ if (this.parser_ && this.parser_.banLocation) {
2630
+ this.parser_.banLocation(oldUrl);
2631
+ }
2632
+ };
2633
+
2968
2634
  return new shaka.net.NetworkingEngine(
2969
- onProgressUpdated_, onHeadersReceived_, onDownloadFailed_, onRequest_);
2635
+ onProgressUpdated_, onHeadersReceived_, onDownloadFailed_, onRequest_,
2636
+ onRetry_);
2970
2637
  }
2971
2638
 
2972
2639
  /**
@@ -3050,18 +2717,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3050
2717
  * Initialize and start the buffering system (observer and timer) so that we
3051
2718
  * can monitor our buffer lead during playback.
3052
2719
  *
2720
+ * @param {!HTMLMediaElement} mediaElement
3053
2721
  * @param {number} rebufferingGoal
3054
2722
  * @private
3055
2723
  */
3056
- startBufferManagement_(rebufferingGoal) {
2724
+ startBufferManagement_(mediaElement, rebufferingGoal) {
3057
2725
  goog.asserts.assert(
3058
2726
  !this.bufferObserver_,
3059
2727
  'No buffering observer should exist before initialization.');
3060
2728
 
3061
- goog.asserts.assert(
3062
- !this.bufferPoller_,
3063
- 'No buffer timer should exist before initialization.');
3064
-
3065
2729
  // Give dummy values, will be updated below.
3066
2730
  this.bufferObserver_ = new shaka.media.BufferingObserver(1, 2);
3067
2731
 
@@ -3071,12 +2735,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3071
2735
  this.updateBufferingSettings_(rebufferingGoal);
3072
2736
  this.updateBufferState_();
3073
2737
 
3074
- // TODO: We should take some time to look into the effects of our
3075
- // quarter-second refresh practice. We often use a quarter-second
3076
- // but we have no documentation about why.
3077
- this.bufferPoller_ = new shaka.util.Timer(() => {
3078
- this.pollBufferState_();
3079
- }).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_());
3080
2746
  }
3081
2747
 
3082
2748
  /**
@@ -3651,17 +3317,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3651
3317
 
3652
3318
  /**
3653
3319
  * Get the range of time (in seconds) that seeking is allowed. If the player
3654
- * 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.
3655
3322
  *
3656
3323
  * @return {{start: number, end: number}}
3657
3324
  * @export
3658
3325
  */
3659
3326
  seekRange() {
3660
- if (!this.fullyLoaded_) {
3661
- return {'start': 0, 'end': 0};
3662
- }
3663
-
3664
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
+ }
3665
3336
  const timeline = this.manifest_.presentationTimeline;
3666
3337
 
3667
3338
  return {
@@ -3965,13 +3636,82 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3965
3636
  * @export
3966
3637
  */
3967
3638
  getImageTracks() {
3639
+ const StreamUtils = shaka.util.StreamUtils;
3640
+ let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
3968
3641
  if (this.manifest_) {
3969
- const imageStreams = this.manifest_.imageStreams;
3970
- const StreamUtils = shaka.util.StreamUtils;
3971
- return imageStreams.map((image) => StreamUtils.imageStreamToTrack(image));
3972
- } else {
3973
- 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;
3974
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;
3711
+ }
3712
+ const columns = parseInt(match[1], 10);
3713
+ const rows = parseInt(match[2], 10);
3714
+ return {columns, rows};
3975
3715
  }
3976
3716
 
3977
3717
  /**
@@ -3985,84 +3725,83 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3985
3725
  * @export
3986
3726
  */
3987
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_;
3988
3733
  if (this.manifest_) {
3989
- const imageStream = this.manifest_.imageStreams.find(
3990
- (stream) => stream.id == trackId);
3991
- if (!imageStream) {
3992
- return null;
3993
- }
3994
- if (!imageStream.segmentIndex) {
3995
- await imageStream.createSegmentIndex();
3996
- }
3997
- const referencePosition = imageStream.segmentIndex.find(time);
3998
- if (referencePosition == null) {
3999
- return null;
4000
- }
4001
- const reference = imageStream.segmentIndex.get(referencePosition);
4002
- const tilesLayout =
4003
- reference.getTilesLayout() || imageStream.tilesLayout;
4004
- // This expression is used to detect one or more numbers (0-9) followed
4005
- // by an x and after one or more numbers (0-9)
4006
- const match = /(\d+)x(\d+)/.exec(tilesLayout);
4007
- if (!match) {
4008
- shaka.log.warning('Tiles layout does not contain a valid format ' +
4009
- ' (columns x rows)');
4010
- return null;
4011
- }
4012
- const fullImageWidth = imageStream.width || 0;
4013
- const fullImageHeight = imageStream.height || 0;
4014
- const columns = parseInt(match[1], 10);
4015
- const rows = parseInt(match[2], 10);
4016
- let width = fullImageWidth / columns;
4017
- let height = fullImageHeight / rows;
4018
- const totalImages = columns * rows;
4019
- const segmentDuration = reference.trueEndTime - reference.startTime;
4020
- const thumbnailDuration =
4021
- reference.getTileDuration() || (segmentDuration / totalImages);
4022
- let thumbnailTime = reference.startTime;
4023
- let positionX = 0;
4024
- let positionY = 0;
4025
- // If the number of images in the segment is greater than 1, we have to
4026
- // find the correct image. For that we will return to the app the
4027
- // coordinates of the position of the correct image.
4028
- // Image search is always from left to right and top to bottom.
4029
- // Note: The time between images within the segment is always
4030
- // equidistant.
4031
- //
4032
- // Eg: Total images 5, tileLayout 5x1, segmentDuration 5, thumbnailTime 2
4033
- // positionX = 0.4 * fullImageWidth
4034
- // positionY = 0
4035
- if (totalImages > 1) {
4036
- const thumbnailPosition =
4037
- Math.floor((time - reference.startTime) / thumbnailDuration);
4038
- thumbnailTime = reference.startTime +
4039
- (thumbnailPosition * thumbnailDuration);
4040
- positionX = (thumbnailPosition % columns) * width;
4041
- positionY = Math.floor(thumbnailPosition / columns) * height;
4042
- }
4043
- let sprite = false;
4044
- const thumbnailSprite = reference.getThumbnailSprite();
4045
- if (thumbnailSprite) {
4046
- sprite = true;
4047
- height = thumbnailSprite.height;
4048
- positionX = thumbnailSprite.positionX;
4049
- positionY = thumbnailSprite.positionY;
4050
- width = thumbnailSprite.width;
4051
- }
4052
- return {
4053
- imageHeight: fullImageHeight,
4054
- imageWidth: fullImageWidth,
4055
- height: height,
4056
- positionX: positionX,
4057
- positionY: positionY,
4058
- startTime: thumbnailTime,
4059
- duration: thumbnailDuration,
4060
- uris: reference.getUris(),
4061
- width: width,
4062
- sprite: sprite,
4063
- };
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;
4064
3791
  }
4065
- 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
+ };
4066
3805
  }
4067
3806
 
4068
3807
  /**
@@ -4179,7 +3918,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4179
3918
  // When track is selected, back-propagate the language to
4180
3919
  // currentAudioLanguage_.
4181
3920
  this.currentAdaptationSetCriteria_ = new shaka.media.ExampleBasedCriteria(
4182
- variant);
3921
+ variant,
3922
+ this.config_.mediaSource.codecSwitchingStrategy,
3923
+ this.config_.manifest.dash.enableAudioGroups);
4183
3924
 
4184
3925
  // Update AbrManager variants to match these new settings.
4185
3926
  this.updateAbrManagerVariants_();
@@ -4255,9 +3996,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4255
3996
  selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0) {
4256
3997
  if (this.manifest_ && this.playhead_) {
4257
3998
  this.currentAdaptationSetCriteria_ =
4258
- new shaka.media.PreferenceBasedCriteria(language, role || '',
4259
- channelsCount, /* hdrLevel= */ '', /* label= */ '',
4260
- this.config_.mediaSource.codecSwitchingStrategy);
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);
4261
4008
 
4262
4009
  const diff = (a, b) => {
4263
4010
  if (!a.video && !b.video) {
@@ -4374,8 +4121,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4374
4121
  // label have the same language.
4375
4122
  this.currentAdaptationSetCriteria_ =
4376
4123
  new shaka.media.PreferenceBasedCriteria(
4377
- firstVariantWithLabel.language, '', 0, '', label,
4378
- this.config_.mediaSource.codecSwitchingStrategy);
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);
4379
4132
 
4380
4133
  this.chooseVariantAndSwitch_(clearBuffer, safeMargin);
4381
4134
  } else if (this.video_ && this.video_.audioTracks) {
@@ -4403,7 +4156,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4403
4156
  isTextTrackVisible() {
4404
4157
  const expected = this.isTextVisible_;
4405
4158
 
4406
- if (this.mediaSourceEngine_) {
4159
+ if (this.mediaSourceEngine_ &&
4160
+ this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
4407
4161
  // Make sure our values are still in-sync.
4408
4162
  const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible();
4409
4163
  goog.asserts.assert(
@@ -4444,6 +4198,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4444
4198
  * @export
4445
4199
  */
4446
4200
  getChapters(language) {
4201
+ if (!this.video_ || !this.video_.src || !this.video_.textTracks) {
4202
+ return [];
4203
+ }
4447
4204
  const LanguageUtils = shaka.util.LanguageUtils;
4448
4205
  const inputlanguage = LanguageUtils.normalize(language);
4449
4206
  const chaptersTracks = this.getChaptersTracks_();
@@ -4602,22 +4359,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4602
4359
  return null;
4603
4360
  }
4604
4361
 
4605
- const walkerPayload = this.walker_.getCurrentPayload();
4606
-
4607
4362
  let presentationTime = 0;
4608
4363
  if (this.playhead_) {
4609
4364
  presentationTime = this.playhead_.getTime();
4610
- } else if (walkerPayload) {
4611
- if (walkerPayload.startTime == null) {
4612
- // A live stream with no requested start time and no playhead yet. We
4613
- // would start at the live edge, but we don't have that yet, so return
4614
- // the current date & time.
4615
- return new Date();
4616
- } else {
4617
- // A specific start time has been requested. This is what Playhead will
4618
- // use once it is created.
4619
- presentationTime = walkerPayload.startTime;
4620
- }
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_;
4621
4374
  }
4622
4375
 
4623
4376
  if (this.manifest_) {
@@ -4842,9 +4595,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4842
4595
  }
4843
4596
  await this.addSrcTrackElement_(uri, language, kind, mimeType, label || '',
4844
4597
  adCuePoints);
4598
+
4599
+ const LanguageUtils = shaka.util.LanguageUtils;
4600
+ const languageNormalized = LanguageUtils.normalize(language);
4601
+
4845
4602
  const textTracks = this.getTextTracks();
4846
4603
  const srcTrack = textTracks.find((t) => {
4847
- return t.language == language &&
4604
+ return LanguageUtils.normalize(t.language) == languageNormalized &&
4848
4605
  t.label == (label || '') &&
4849
4606
  t.kind == kind;
4850
4607
  });
@@ -4863,7 +4620,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4863
4620
 
4864
4621
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
4865
4622
 
4866
- const duration = this.manifest_.presentationTimeline.getDuration();
4623
+ let duration = this.video_.duration;
4624
+ if (this.manifest_) {
4625
+ duration = this.manifest_.presentationTimeline.getDuration();
4626
+ }
4867
4627
  if (duration == Infinity) {
4868
4628
  throw new shaka.util.Error(
4869
4629
  shaka.util.Error.Severity.RECOVERABLE,
@@ -4914,6 +4674,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4914
4674
  closedCaptions: null,
4915
4675
  accessibilityPurpose: null,
4916
4676
  external: true,
4677
+ fastSwitching: false,
4917
4678
  };
4918
4679
 
4919
4680
  const fullMimeType = shaka.util.MimeUtils.getFullType(
@@ -4957,14 +4718,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4957
4718
  shaka.util.Error.Code.CONTENT_NOT_LOADED);
4958
4719
  }
4959
4720
 
4960
- if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
4961
- shaka.log.error('Cannot add this thumbnail track when loaded with src=');
4962
- throw new shaka.util.Error(
4963
- shaka.util.Error.Severity.RECOVERABLE,
4964
- shaka.util.Error.Category.TEXT,
4965
- shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_SRC_EQUALS);
4966
- }
4967
-
4968
4721
  if (!mimeType) {
4969
4722
  mimeType = await this.getTextMimetype_(uri);
4970
4723
  }
@@ -4978,8 +4731,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4978
4731
  }
4979
4732
 
4980
4733
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
4981
-
4982
- const duration = this.manifest_.presentationTimeline.getDuration();
4734
+ let duration = this.video_.duration;
4735
+ if (this.manifest_) {
4736
+ duration = this.manifest_.presentationTimeline.getDuration();
4737
+ }
4983
4738
  if (duration == Infinity) {
4984
4739
  throw new shaka.util.Error(
4985
4740
  shaka.util.Error.Severity.RECOVERABLE,
@@ -5009,7 +4764,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5009
4764
  vttOffset: 0,
5010
4765
  };
5011
4766
  const data = shaka.util.BufferUtils.toUint8(buffer);
5012
- const cues = TextParser.parseMedia(data, time);
4767
+ const cues = TextParser.parseMedia(data, time, uri);
5013
4768
 
5014
4769
  const references = [];
5015
4770
  for (const cue of cues) {
@@ -5069,9 +4824,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5069
4824
  tilesLayout: '1x1',
5070
4825
  accessibilityPurpose: null,
5071
4826
  external: true,
4827
+ fastSwitching: false,
5072
4828
  };
5073
4829
 
5074
- this.manifest_.imageStreams.push(stream);
4830
+ if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
4831
+ this.externalSrcEqualsThumbnailsStreams_.push(stream);
4832
+ } else {
4833
+ this.manifest_.imageStreams.push(stream);
4834
+ }
5075
4835
  this.onTracksChanged_();
5076
4836
  return shaka.util.StreamUtils.imageStreamToTrack(stream);
5077
4837
  }
@@ -5133,6 +4893,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5133
4893
  });
5134
4894
  });
5135
4895
 
4896
+ this.onTracksChanged_();
4897
+
5136
4898
  return chaptersTrack;
5137
4899
  }
5138
4900
 
@@ -5151,19 +4913,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5151
4913
  * @private
5152
4914
  */
5153
4915
  async getTextMimetype_(uri) {
5154
- // Try using the uri extension.
5155
- const extension = shaka.media.ManifestParser.getExtension(uri);
5156
- let mimeType = shaka.Player.TEXT_EXTENSIONS_TO_MIME_TYPES_[extension];
5157
-
5158
- if (mimeType) {
5159
- return mimeType;
5160
- }
5161
-
4916
+ let mimeType;
5162
4917
  try {
5163
4918
  goog.asserts.assert(
5164
4919
  this.networkingEngine_, 'Need networking engine.');
5165
4920
  // eslint-disable-next-line require-atomic-updates
5166
- mimeType = await shaka.media.ManifestParser.getMimeType(uri,
4921
+ mimeType = await shaka.net.NetworkingUtils.getMimeType(uri,
5167
4922
  this.networkingEngine_,
5168
4923
  this.config_.streaming.retryParameters);
5169
4924
  } catch (error) {}
@@ -5174,12 +4929,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5174
4929
 
5175
4930
  shaka.log.error(
5176
4931
  'The mimeType has not been provided and it could not be deduced ' +
5177
- 'from its extension.');
4932
+ 'from its uri.');
5178
4933
  throw new shaka.util.Error(
5179
4934
  shaka.util.Error.Severity.RECOVERABLE,
5180
4935
  shaka.util.Error.Category.TEXT,
5181
4936
  shaka.util.Error.Code.TEXT_COULD_NOT_GUESS_MIME_TYPE,
5182
- extension);
4937
+ uri);
5183
4938
  }
5184
4939
 
5185
4940
  /**
@@ -5268,7 +5023,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5268
5023
  vttOffset: 0,
5269
5024
  };
5270
5025
  const data = shaka.util.BufferUtils.toUint8(buffer);
5271
- const cues = obj.parseMedia(data, time);
5026
+ const cues = obj.parseMedia(data, time, /* uri= */ null);
5272
5027
  return shaka.text.WebVttGenerator.convert(cues, adCuePoints);
5273
5028
  }
5274
5029
  throw new shaka.util.Error(
@@ -5387,7 +5142,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5387
5142
  return new shaka.text.UITextDisplayer(
5388
5143
  this.video_, this.videoContainer_);
5389
5144
  } else {
5390
- return new shaka.text.SimpleTextDisplayer(this.video_);
5145
+ // eslint-disable-next-line no-restricted-syntax
5146
+ if (HTMLMediaElement.prototype.addTextTrack) {
5147
+ return new shaka.text.SimpleTextDisplayer(this.video_);
5148
+ } else {
5149
+ shaka.log.warning('Text tracks are not supported by the ' +
5150
+ 'browser, disabling.');
5151
+ return new shaka.text.StubTextDisplayer();
5152
+ }
5391
5153
  }
5392
5154
  };
5393
5155
  return config;
@@ -5491,6 +5253,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5491
5253
  closedCaptions: null,
5492
5254
  accessibilityPurpose: null,
5493
5255
  external: false,
5256
+ fastSwitching: false,
5494
5257
  };
5495
5258
  manifest.textStreams.push(textStream);
5496
5259
  closedCaptionsSet.add(id);
@@ -5519,15 +5282,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5519
5282
  */
5520
5283
  async filterManifestWithStreamUtils_(manifest) {
5521
5284
  goog.asserts.assert(manifest, 'Manifest should exist!');
5522
- goog.asserts.assert(this.video_, 'Must not be destroyed');
5523
-
5524
- /** @type {?shaka.extern.Variant} */
5525
- const currentVariant = this.streamingEngine_ ?
5526
- this.streamingEngine_.getCurrentVariant() : null;
5527
-
5528
- await shaka.util.StreamUtils.filterManifest(
5529
- this.drmEngine_, currentVariant, manifest,
5530
- this.config_.mediaSource.codecSwitchingStrategy);
5285
+ await shaka.util.StreamUtils.filterManifest(this.drmEngine_, manifest);
5531
5286
  this.checkPlayableVariants_(manifest);
5532
5287
  }
5533
5288
 
@@ -5652,7 +5407,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5652
5407
  const loaded = this.stats_ && this.bufferObserver_ && this.playhead_;
5653
5408
 
5654
5409
  if (loaded) {
5655
- this.playRateController_.setBuffering(isBuffering);
5656
5410
  if (this.cmcdManager_) {
5657
5411
  this.cmcdManager_.setBuffering(isBuffering);
5658
5412
  }
@@ -5773,6 +5527,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5773
5527
  }
5774
5528
  }
5775
5529
 
5530
+ let liveSyncMinLatency;
5531
+ let liveSyncMinPlaybackRate;
5532
+ if (this.config_.streaming.liveSync) {
5533
+ liveSyncMinLatency = this.config_.streaming.liveSyncMinLatency;
5534
+ liveSyncMinPlaybackRate = this.config_.streaming.liveSyncMinPlaybackRate;
5535
+ } else {
5536
+ // serviceDescription must override if it is defined in the MPD and
5537
+ // liveSync configuration is not set.
5538
+ if (this.manifest_ && this.manifest_.serviceDescription) {
5539
+ liveSyncMinLatency = this.manifest_.serviceDescription.minLatency ||
5540
+ this.config_.streaming.liveSyncMinLatency;
5541
+ liveSyncMinPlaybackRate =
5542
+ this.manifest_.serviceDescription.minPlaybackRate ||
5543
+ this.config_.streaming.liveSyncMinPlaybackRate;
5544
+ }
5545
+ }
5546
+
5776
5547
  const playbackRate = this.video_.playbackRate;
5777
5548
  const latency = seekRange.end - this.video_.currentTime;
5778
5549
  let offset = 0;
@@ -5796,11 +5567,62 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5796
5567
  'Updating playbackRate to ' + liveSyncPlaybackRate);
5797
5568
  this.trickPlay(liveSyncPlaybackRate);
5798
5569
  }
5799
- } else if (playbackRate !== 1 && playbackRate !== 0) {
5570
+ } else if (liveSyncMinLatency && liveSyncMinPlaybackRate &&
5571
+ (latency - offset) < liveSyncMinLatency) {
5572
+ if (playbackRate != liveSyncMinPlaybackRate) {
5573
+ shaka.log.debug('Latency (' + latency + 's) ' +
5574
+ 'is smaller than liveSyncMinLatency (' + liveSyncMinLatency + 's). ' +
5575
+ 'Updating playbackRate to ' + liveSyncMinPlaybackRate);
5576
+ this.trickPlay(liveSyncMinPlaybackRate);
5577
+ }
5578
+ } else if (playbackRate !== this.playRateController_.getDefaultRate()) {
5800
5579
  this.cancelTrickPlay();
5801
5580
  }
5802
5581
  }
5803
5582
 
5583
+ /**
5584
+ * Callback for video progress events
5585
+ *
5586
+ * @private
5587
+ */
5588
+ onVideoProgress_() {
5589
+ if (!this.video_) {
5590
+ return;
5591
+ }
5592
+ let hasNewCompletionPercent = false;
5593
+ const completionRatio = this.video_.currentTime / this.video_.duration;
5594
+ if (!isNaN(completionRatio)) {
5595
+ const percent = Math.round(100 * completionRatio);
5596
+ if (isNaN(this.completionPercent_)) {
5597
+ this.completionPercent_ = percent;
5598
+ hasNewCompletionPercent = true;
5599
+ } else {
5600
+ const newCompletionPercent = Math.max(this.completionPercent_, percent);
5601
+ if (this.completionPercent_ != newCompletionPercent) {
5602
+ this.completionPercent_ = newCompletionPercent;
5603
+ hasNewCompletionPercent = true;
5604
+ }
5605
+ }
5606
+ }
5607
+ if (hasNewCompletionPercent) {
5608
+ let event;
5609
+ if (this.completionPercent_ == 0) {
5610
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.Started);
5611
+ } else if (this.completionPercent_ == 25) {
5612
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.FirstQuartile);
5613
+ } else if (this.completionPercent_ == 50) {
5614
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.Midpoint);
5615
+ } else if (this.completionPercent_ == 75) {
5616
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.ThirdQuartile);
5617
+ } else if (this.completionPercent_ == 100) {
5618
+ event = this.makeEvent_(shaka.util.FakeEvent.EventName.Complete);
5619
+ }
5620
+ if (event) {
5621
+ this.dispatchEvent(event);
5622
+ }
5623
+ }
5624
+ }
5625
+
5804
5626
  /**
5805
5627
  * Callback from Playhead.
5806
5628
  *
@@ -5855,12 +5677,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5855
5677
  *
5856
5678
  * On error, this dispatches an error event and returns null.
5857
5679
  *
5680
+ * @param {boolean=} initialSelection
5858
5681
  * @return {?shaka.extern.Variant}
5859
5682
  * @private
5860
5683
  */
5861
- chooseVariant_() {
5684
+ chooseVariant_(initialSelection = false) {
5862
5685
  if (this.updateAbrManagerVariants_()) {
5863
- return this.abrManager_.chooseVariant();
5686
+ return this.abrManager_.chooseVariant(initialSelection);
5864
5687
  } else {
5865
5688
  return null;
5866
5689
  }
@@ -6901,8 +6724,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6901
6724
  // Because Safari's native HLS reports slightly inaccurate values for
6902
6725
  // bufferEnd here, we use a fudge factor. Without this, we can end up in a
6903
6726
  // buffering state at the end of the stream. See issue #2117.
6904
- // TODO: Try to remove the fudge here once we no longer manage buffering
6905
- // state above the browser with playbackRate=0.
6906
6727
  const fudge = 1; // 1000 ms
6907
6728
  return bufferEnd != null && bufferEnd >= this.video_.duration - fudge;
6908
6729
  }
@@ -6919,291 +6740,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6919
6740
  shaka.util.Error.Category.PLAYER,
6920
6741
  shaka.util.Error.Code.LOAD_INTERRUPTED);
6921
6742
  }
6922
-
6923
- /**
6924
- * Key
6925
- * ----------------------
6926
- * D : Detach Node
6927
- * A : Attach Node
6928
- * MS : Media Source Node
6929
- * P : Manifest Parser Node
6930
- * M : Manifest Node
6931
- * DRM : Drm Engine Node
6932
- * L : Load Node
6933
- * U : Unloading Node
6934
- * SRC : Src Equals Node
6935
- *
6936
- * Graph Topology
6937
- * ----------------------
6938
- *
6939
- * [SRC]-----+
6940
- * ^ |
6941
- * | v
6942
- * [D]<-->[A]<-----[U]
6943
- * | ^
6944
- * v |
6945
- * [MS]------+
6946
- * | |
6947
- * v |
6948
- * [P]-------+
6949
- * | |
6950
- * v |
6951
- * [M]-------+
6952
- * | |
6953
- * v |
6954
- * [DRM]-----+
6955
- * | |
6956
- * v |
6957
- * [L]-------+
6958
- *
6959
- * @param {!shaka.routing.Node} currentlyAt
6960
- * @param {shaka.routing.Payload} currentlyWith
6961
- * @param {!shaka.routing.Node} wantsToBeAt
6962
- * @param {shaka.routing.Payload} wantsToHave
6963
- * @return {?shaka.routing.Node}
6964
- * @private
6965
- */
6966
- getNextStep_(currentlyAt, currentlyWith, wantsToBeAt, wantsToHave) {
6967
- let next = null;
6968
-
6969
- // Detach is very simple, either stay in detach (because |detach| was called
6970
- // while in detached) or go somewhere that requires us to attach to an
6971
- // element.
6972
- if (currentlyAt == this.detachNode_) {
6973
- next = wantsToBeAt == this.detachNode_ ?
6974
- this.detachNode_ :
6975
- this.attachNode_;
6976
- }
6977
-
6978
- if (currentlyAt == this.attachNode_) {
6979
- next = this.getNextAfterAttach_(wantsToBeAt, currentlyWith, wantsToHave);
6980
- }
6981
-
6982
- if (currentlyAt == this.mediaSourceNode_) {
6983
- next = this.getNextAfterMediaSource_(
6984
- wantsToBeAt, currentlyWith, wantsToHave);
6985
- }
6986
-
6987
- if (currentlyAt == this.parserNode_) {
6988
- next = this.getNextMatchingAllDependencies_(
6989
- /* destination= */ this.loadNode_,
6990
- /* next= */ this.manifestNode_,
6991
- /* reset= */ this.unloadNode_,
6992
- /* goingTo= */ wantsToBeAt,
6993
- /* has= */ currentlyWith,
6994
- /* wants= */ wantsToHave);
6995
- }
6996
-
6997
- if (currentlyAt == this.manifestNode_) {
6998
- next = this.getNextMatchingAllDependencies_(
6999
- /* destination= */ this.loadNode_,
7000
- /* next= */ this.drmNode_,
7001
- /* reset= */ this.unloadNode_,
7002
- /* goingTo= */ wantsToBeAt,
7003
- /* has= */ currentlyWith,
7004
- /* wants= */ wantsToHave);
7005
- }
7006
-
7007
- // For DRM, we have two options "load" or "unload". If all our constraints
7008
- // are met, we can go to "load". If anything is off, we must go back to
7009
- // "unload" to reset.
7010
- if (currentlyAt == this.drmNode_) {
7011
- next = this.getNextMatchingAllDependencies_(
7012
- /* destination= */ this.loadNode_,
7013
- /* next= */ this.loadNode_,
7014
- /* reset= */ this.unloadNode_,
7015
- /* goingTo= */ wantsToBeAt,
7016
- /* has= */ currentlyWith,
7017
- /* wants= */ wantsToHave);
7018
- }
7019
-
7020
- // For DRM w/ src= playback, we only care about destination and media
7021
- // element.
7022
- if (currentlyAt == this.srcEqualsDrmNode_) {
7023
- if (wantsToBeAt == this.srcEqualsNode_ &&
7024
- currentlyWith.mediaElement == wantsToHave.mediaElement) {
7025
- next = this.srcEqualsNode_;
7026
- } else {
7027
- next = this.unloadNode_;
7028
- }
7029
- }
7030
-
7031
- // After we load content, always go through unload because we can't safely
7032
- // use components after we have started playback.
7033
- if (currentlyAt == this.loadNode_ || currentlyAt == this.srcEqualsNode_) {
7034
- next = this.unloadNode_;
7035
- }
7036
-
7037
- if (currentlyAt == this.unloadNode_) {
7038
- next = this.getNextAfterUnload_(wantsToBeAt, currentlyWith, wantsToHave);
7039
- }
7040
-
7041
- goog.asserts.assert(next, 'Missing next step!');
7042
- return next;
7043
- }
7044
-
7045
- /**
7046
- * @param {!shaka.routing.Node} goingTo
7047
- * @param {shaka.routing.Payload} has
7048
- * @param {shaka.routing.Payload} wants
7049
- * @return {?shaka.routing.Node}
7050
- * @private
7051
- */
7052
- getNextAfterAttach_(goingTo, has, wants) {
7053
- // Attach and detach are the only two nodes that we can directly go
7054
- // back-and-forth between.
7055
- if (goingTo == this.detachNode_) {
7056
- return this.detachNode_;
7057
- }
7058
-
7059
- // If we are going anywhere other than detach, then we need the media
7060
- // element to match, if they don't match, we need to go through detach
7061
- // first.
7062
- if (has.mediaElement != wants.mediaElement) {
7063
- return this.detachNode_;
7064
- }
7065
-
7066
- // If we are already in attached, and someone calls |attach| again (to the
7067
- // same video element), we can handle the redundant request by re-entering
7068
- // our current state.
7069
- if (goingTo == this.attachNode_) {
7070
- return this.attachNode_;
7071
- }
7072
-
7073
- // The next step from attached to loaded is through media source.
7074
- if (goingTo == this.mediaSourceNode_ || goingTo == this.loadNode_) {
7075
- return this.mediaSourceNode_;
7076
- }
7077
-
7078
- // If we are going to src=, then we should set up DRM first. This will
7079
- // support cases like FairPlay HLS on Safari.
7080
- if (goingTo == this.srcEqualsNode_) {
7081
- return this.srcEqualsDrmNode_;
7082
- }
7083
-
7084
- // We are missing a rule, the null will get caught by a common check in
7085
- // the routing system.
7086
- return null;
7087
- }
7088
-
7089
- /**
7090
- * @param {!shaka.routing.Node} goingTo
7091
- * @param {shaka.routing.Payload} has
7092
- * @param {shaka.routing.Payload} wants
7093
- * @return {?shaka.routing.Node}
7094
- * @private
7095
- */
7096
- getNextAfterMediaSource_(goingTo, has, wants) {
7097
- // We can only go to parse manifest or unload. If we want to go to load and
7098
- // we have the right media element, we can go to parse manifest. If we
7099
- // don't, no matter where we want to go, we must go through unload.
7100
- if (goingTo == this.loadNode_ && has.mediaElement == wants.mediaElement) {
7101
- return this.parserNode_;
7102
- }
7103
-
7104
- // Right now the unload node is responsible for tearing down all playback
7105
- // components (including media source). So since we have created media
7106
- // source, we need to unload since our dependencies are not compatible.
7107
- //
7108
- // TODO: We are structured this way to maintain a historic structure. Going
7109
- // forward, there is no reason to restrict ourselves to this. Going
7110
- // forward we should explore breaking apart |onUnload| and develop
7111
- // more meaningful terminology around tearing down playback resources.
7112
- return this.unloadNode_;
7113
- }
7114
-
7115
- /**
7116
- * After unload there are only two options, attached or detached. This choice
7117
- * is based on whether or not we have a media element. If we have a media
7118
- * element, then we go to attach. If we don't have a media element, we go to
7119
- * detach.
7120
- *
7121
- * @param {!shaka.routing.Node} goingTo
7122
- * @param {shaka.routing.Payload} has
7123
- * @param {shaka.routing.Payload} wants
7124
- * @return {?shaka.routing.Node}
7125
- * @private
7126
- */
7127
- getNextAfterUnload_(goingTo, has, wants) {
7128
- // If we don't want a media element, detach.
7129
- // If we have the wrong media element, detach.
7130
- // Otherwise it means we want to attach to a media element and it is safe to
7131
- // do so.
7132
- return !wants.mediaElement || has.mediaElement != wants.mediaElement ?
7133
- this.detachNode_ :
7134
- this.attachNode_;
7135
- }
7136
-
7137
- /**
7138
- * A general method used to handle routing when we can either than one step
7139
- * toward our destination (while all our dependencies match) or go to a node
7140
- * that will reset us so we can try again.
7141
- *
7142
- * @param {!shaka.routing.Node} destinationNode
7143
- * What |goingTo| must be for us to step toward |nextNode|. Otherwise we
7144
- * will go to |resetNode|.
7145
- * @param {!shaka.routing.Node} nextNode
7146
- * The node we will go to next if |goingTo == destinationNode| and all
7147
- * dependencies match.
7148
- * @param {!shaka.routing.Node} resetNode
7149
- * The node we will go to next if |goingTo != destinationNode| or any
7150
- * dependency does not match.
7151
- * @param {!shaka.routing.Node} goingTo
7152
- * The node that the walker is trying to go to.
7153
- * @param {shaka.routing.Payload} has
7154
- * The payload that the walker currently has.
7155
- * @param {shaka.routing.Payload} wants
7156
- * The payload that the walker wants to have when iy gets to |goingTo|.
7157
- * @return {shaka.routing.Node}
7158
- * @private
7159
- */
7160
- getNextMatchingAllDependencies_(destinationNode, nextNode, resetNode, goingTo,
7161
- has, wants) {
7162
- if (goingTo == destinationNode &&
7163
- has.mediaElement == wants.mediaElement &&
7164
- has.uri == wants.uri &&
7165
- has.mimeType == wants.mimeType) {
7166
- return nextNode;
7167
- }
7168
-
7169
- return resetNode;
7170
- }
7171
-
7172
- /**
7173
- * @return {shaka.routing.Payload}
7174
- * @private
7175
- */
7176
- static createEmptyPayload_() {
7177
- return {
7178
- mediaElement: null,
7179
- mimeType: null,
7180
- startTime: null,
7181
- startTimeOfLoad: NaN,
7182
- uri: null,
7183
- keepAdManager: true,
7184
- };
7185
- }
7186
-
7187
- /**
7188
- * Using a promise, wrap the listeners returned by |Walker.startNewRoute|.
7189
- * This will work for most usages in |Player| but should not be used for
7190
- * special cases.
7191
- *
7192
- * This will connect |onCancel|, |onEnd|, |onError|, and |onSkip| with
7193
- * |resolve| and |reject| but will leave |onStart| unset.
7194
- *
7195
- * @param {shaka.routing.Walker.Listeners} listeners
7196
- * @return {!Promise}
7197
- * @private
7198
- */
7199
- wrapWalkerListenersWithPromise_(listeners) {
7200
- return new Promise((resolve, reject) => {
7201
- listeners.onCancel = () => reject(this.createAbortLoadError_());
7202
- listeners.onEnd = () => resolve();
7203
- listeners.onError = (e) => reject(e);
7204
- listeners.onSkip = () => reject(this.createAbortLoadError_());
7205
- });
7206
- }
7207
6743
  };
7208
6744
 
7209
6745
  /**
@@ -7245,7 +6781,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
7245
6781
  * @export
7246
6782
  */
7247
6783
  // eslint-disable-next-line no-useless-concat
7248
- shaka.Player.version = 'v4.5.0' + '-uncompiled'; // x-release-please-version
6784
+ shaka.Player.version = 'v4.6.1' + '-uncompiled'; // x-release-please-version
7249
6785
 
7250
6786
  // Initialize the deprecation system using the version string we just set
7251
6787
  // on the player.
@@ -7272,47 +6808,6 @@ shaka.Player.supportPlugins_ = {};
7272
6808
  shaka.Player.adManagerFactory_ = null;
7273
6809
 
7274
6810
 
7275
- /**
7276
- * @const {!Object.<string, string>}
7277
- * @private
7278
- */
7279
- shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_ = {
7280
- 'mp4': 'video/mp4',
7281
- 'm4v': 'video/mp4',
7282
- 'm4a': 'audio/mp4',
7283
- 'webm': 'video/webm',
7284
- 'weba': 'audio/webm',
7285
- 'mkv': 'video/webm', // Chromium browsers supports it.
7286
- 'ts': 'video/mp2t',
7287
- 'ogv': 'video/ogg',
7288
- 'ogg': 'audio/ogg',
7289
- 'mpg': 'video/mpeg',
7290
- 'mpeg': 'video/mpeg',
7291
- 'm3u8': 'application/x-mpegurl',
7292
- 'mpd': 'application/dash+xml',
7293
- 'mp3': 'audio/mpeg',
7294
- 'aac': 'audio/aac',
7295
- 'flac': 'audio/flac',
7296
- 'wav': 'audio/wav',
7297
- };
7298
-
7299
-
7300
- /**
7301
- * @const {!Object.<string, string>}
7302
- * @private
7303
- */
7304
- shaka.Player.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
7305
- 'sbv': 'text/x-subviewer',
7306
- 'srt': 'text/srt',
7307
- 'vtt': 'text/vtt',
7308
- 'webvtt': 'text/vtt',
7309
- 'ttml': 'application/ttml+xml',
7310
- 'lrc': 'application/x-subtitle-lrc',
7311
- 'ssa': 'text/x-ssa',
7312
- 'ass': 'text/x-ssa',
7313
- };
7314
-
7315
-
7316
6811
  /**
7317
6812
  * @const {string}
7318
6813
  */