shaka-player 2.5.1 → 2.5.5

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 (166) hide show
  1. package/.csslintrc +13 -0
  2. package/AUTHORS +3 -0
  3. package/CHANGELOG.md +156 -0
  4. package/CONTRIBUTORS +3 -0
  5. package/README.md +3 -0
  6. package/build/README.md +5 -5
  7. package/build/apps.py +3 -1
  8. package/build/build.py +5 -2
  9. package/build/check.py +17 -0
  10. package/build/compiler.py +49 -0
  11. package/build/conformance.textproto +1 -0
  12. package/build/docs.py +0 -6
  13. package/build/generateExterns.js +72 -20
  14. package/build/install-linux-prereqs.sh +4 -4
  15. package/build/shakaBuildHelpers.py +7 -8
  16. package/build/test.py +21 -9
  17. package/build/types/core +1 -0
  18. package/build/types/ui +6 -1
  19. package/demo/asset_card.js +129 -41
  20. package/demo/cast_receiver/receiver_app.css +27 -13
  21. package/demo/close_button.js +10 -15
  22. package/demo/common/asset.js +6 -6
  23. package/demo/common/assets.js +72 -14
  24. package/demo/config.js +28 -25
  25. package/demo/custom.js +20 -16
  26. package/demo/demo.less +107 -16
  27. package/demo/demo_uncompiled.js +33 -0
  28. package/demo/demo_utils.js +4 -3
  29. package/demo/front.js +12 -8
  30. package/demo/index.html +14 -17
  31. package/demo/input.js +30 -23
  32. package/demo/input_container.js +15 -11
  33. package/demo/load.js +5 -7
  34. package/demo/main.js +138 -49
  35. package/demo/search.js +40 -36
  36. package/demo/service_worker.js +1 -1
  37. package/demo/tooltip.js +16 -24
  38. package/dist/.csslintstamp +0 -0
  39. package/dist/controls.css +1 -1
  40. package/dist/controls.css.map +1 -1
  41. package/dist/demo.compiled.debug.js +165 -152
  42. package/dist/demo.compiled.debug.map +4 -4
  43. package/dist/demo.compiled.js +165 -152
  44. package/dist/demo.compiled.map +4 -4
  45. package/dist/demo.css +1 -1
  46. package/dist/demo.css.map +1 -1
  47. package/dist/deps.js +28 -11
  48. package/dist/receiver.compiled.debug.js +11 -11
  49. package/dist/receiver.compiled.debug.map +3 -3
  50. package/dist/receiver.compiled.js +11 -11
  51. package/dist/receiver.compiled.map +3 -3
  52. package/dist/shaka-player.compiled.debug.externs.js +232 -102
  53. package/dist/shaka-player.compiled.debug.js +189 -165
  54. package/dist/shaka-player.compiled.debug.map +4 -4
  55. package/dist/shaka-player.compiled.externs.js +232 -102
  56. package/dist/shaka-player.compiled.js +567 -553
  57. package/dist/shaka-player.compiled.map +4 -4
  58. package/dist/shaka-player.ui.debug.externs.js +338 -113
  59. package/dist/shaka-player.ui.debug.js +296 -276
  60. package/dist/shaka-player.ui.debug.map +4 -4
  61. package/dist/shaka-player.ui.externs.js +338 -113
  62. package/dist/shaka-player.ui.js +685 -671
  63. package/dist/shaka-player.ui.map +4 -4
  64. package/docs/tutorials/drm-config.md +1 -0
  65. package/docs/tutorials/fairplay.md +77 -0
  66. package/docs/tutorials/index.json +1 -0
  67. package/docs/tutorials/manifest-parser.md +1 -1
  68. package/docs/tutorials/ui-customization.md +5 -2
  69. package/docs/tutorials/ui.md +10 -4
  70. package/docs/tutorials/welcome.md +2 -2
  71. package/externs/mediasession.js +16 -1
  72. package/externs/shaka/net.js +14 -4
  73. package/externs/shaka/player.js +136 -90
  74. package/externs/tippy.js +30 -0
  75. package/externs/webkitmediakeys.js +4 -0
  76. package/karma.conf.js +30 -24
  77. package/lib/cast/cast_proxy.js +94 -23
  78. package/lib/cast/cast_receiver.js +8 -1
  79. package/lib/dash/content_protection.js +33 -1
  80. package/lib/dash/dash_parser.js +13 -5
  81. package/lib/hls/hls_parser.js +20 -4
  82. package/lib/media/drm_engine.js +29 -2
  83. package/lib/media/media_source_engine.js +9 -6
  84. package/lib/media/play_rate_controller.js +14 -3
  85. package/lib/media/playhead.js +8 -3
  86. package/lib/media/streaming_engine.js +68 -20
  87. package/lib/media/transmuxer.js +2 -2
  88. package/lib/media/video_wrapper.js +8 -3
  89. package/lib/net/data_uri_plugin.js +5 -2
  90. package/lib/net/http_fetch_plugin.js +3 -3
  91. package/lib/net/http_plugin_utils.js +2 -4
  92. package/lib/net/http_xhr_plugin.js +2 -2
  93. package/lib/net/networking_engine.js +1 -0
  94. package/lib/offline/offline_scheme.js +7 -2
  95. package/lib/player.js +18 -8
  96. package/lib/polyfill/input_event.js +7 -11
  97. package/lib/polyfill/mediasource.js +1 -1
  98. package/lib/polyfill/patchedmediakeys_apple.js +2 -72
  99. package/lib/text/cue.js +1 -1
  100. package/lib/text/ttml_text_parser.js +1 -1
  101. package/lib/util/config_utils.js +1 -0
  102. package/lib/util/error.js +45 -11
  103. package/lib/util/event_manager.js +211 -100
  104. package/lib/util/fairplay_utils.js +122 -0
  105. package/lib/util/i_releasable.js +1 -0
  106. package/lib/util/player_configuration.js +7 -0
  107. package/lib/util/pssh.js +41 -0
  108. package/lib/util/string_utils.js +42 -6
  109. package/lib/util/timer.js +1 -1
  110. package/package.json +33 -33
  111. package/roadmap.md +96 -0
  112. package/shaka-player.uncompiled.js +5 -1
  113. package/test/dash/dash_parser_content_protection_unit.js +35 -1
  114. package/test/dash/dash_parser_manifest_unit.js +60 -0
  115. package/test/hls/hls_live_unit.js +7 -1
  116. package/test/hls/hls_parser_unit.js +50 -0
  117. package/test/media/drm_engine_unit.js +10 -4
  118. package/test/media/media_source_engine_integration.js +12 -0
  119. package/test/media/media_source_engine_unit.js +3 -1
  120. package/test/media/playhead_unit.js +7 -5
  121. package/test/media/streaming_engine_unit.js +46 -1
  122. package/test/media/transmuxer_integration.js +13 -0
  123. package/test/net/data_uri_plugin_unit.js +13 -2
  124. package/test/net/http_plugin_unit.js +17 -12
  125. package/test/net/networking_engine_unit.js +17 -8
  126. package/test/offline/offline_scheme_unit.js +18 -8
  127. package/test/player_external.js +3 -2
  128. package/test/player_integration.js +94 -3
  129. package/test/player_src_equals_integration.js +2 -2
  130. package/test/player_unit.js +21 -8
  131. package/test/test/boot.js +20 -5
  132. package/test/test/externs/jasmine.js +48 -0
  133. package/test/test/util/expect_async_polyfill.js +145 -0
  134. package/test/test/util/fake_networking_engine.js +1 -0
  135. package/test/test/util/manifest_generator.js +2 -1
  136. package/test/test/util/test_scheme.js +34 -2
  137. package/test/test/util/ui_utils.js +41 -0
  138. package/test/test/util/util.js +16 -8
  139. package/test/ui/ui_unit.js +130 -4
  140. package/ui/audio_language_selection.js +17 -96
  141. package/ui/big_play_button.js +61 -0
  142. package/ui/controls.js +226 -335
  143. package/ui/controls.less +3 -5
  144. package/ui/element.js +24 -6
  145. package/ui/externs/ui.js +62 -9
  146. package/ui/fullscreen_button.js +2 -52
  147. package/ui/less/buttons.less +10 -16
  148. package/ui/less/containers.less +75 -20
  149. package/ui/less/general.less +18 -2
  150. package/ui/less/other_elements.less +2 -1
  151. package/ui/less/overflow_menu.less +11 -10
  152. package/ui/less/range_elements.less +12 -10
  153. package/ui/less/spinner.less +2 -0
  154. package/ui/overflow_menu.js +1 -1
  155. package/ui/pip_button.js +1 -1
  156. package/ui/{play_pause_button.js → play_button.js} +17 -47
  157. package/ui/presentation_time.js +17 -12
  158. package/ui/range_element.js +176 -0
  159. package/ui/resolution_selection.js +43 -106
  160. package/ui/seek_bar.js +204 -0
  161. package/ui/settings_menu.js +113 -0
  162. package/ui/small_play_button.js +73 -0
  163. package/ui/text_displayer.js +17 -14
  164. package/ui/text_selection.js +42 -107
  165. package/ui/ui.js +17 -40
  166. package/ui/volume_bar.js +40 -68
package/ui/controls.js CHANGED
@@ -19,12 +19,12 @@
19
19
  goog.provide('shaka.ui.Controls');
20
20
  goog.provide('shaka.ui.ControlsPanel');
21
21
 
22
- goog.require('goog.asserts');
23
22
  goog.require('shaka.log');
24
23
  goog.require('shaka.ui.Constants');
25
24
  goog.require('shaka.ui.Enums');
26
25
  goog.require('shaka.ui.Locales');
27
26
  goog.require('shaka.ui.Localization');
27
+ goog.require('shaka.ui.SeekBar');
28
28
  goog.require('shaka.ui.Utils');
29
29
  goog.require('shaka.util.Dom');
30
30
  goog.require('shaka.util.EventManager');
@@ -54,7 +54,7 @@ shaka.ui.Controls = function(player, videoContainer, video, config) {
54
54
  /** @private {boolean} */
55
55
  this.overrideCssShowControls_ = false;
56
56
 
57
- /** shaka.extern.UIConfiguration */
57
+ /** @private {shaka.extern.UIConfiguration} */
58
58
  this.config_ = config;
59
59
 
60
60
  /** @private {shaka.cast.CastProxy} */
@@ -79,19 +79,14 @@ shaka.ui.Controls = function(player, videoContainer, video, config) {
79
79
  /** @private {!HTMLElement} */
80
80
  this.videoContainer_ = videoContainer;
81
81
 
82
+ /** @private {shaka.ui.SeekBar} */
83
+ this.seekBar_ = null;
84
+
82
85
  /** @private {boolean} */
83
86
  this.isSeeking_ = false;
84
87
 
85
- /**
86
- * This timer is used to introduce a delay between the user scrubbing across
87
- * the seek bar and the seek being sent to the player.
88
- *
89
- * @private {shaka.util.Timer}
90
- */
91
- this.seekTimer_ = new shaka.util.Timer(() => {
92
- goog.asserts.assert(this.seekBar_ != null, 'Seekbar should not be null!');
93
- this.video_.currentTime = parseFloat(this.seekBar_.value);
94
- });
88
+ /** @private {!Array.<!Element>} */
89
+ this.settingsMenus_ = [];
95
90
 
96
91
  /**
97
92
  * This timer is used to detect when the user has stopped moving the mouse
@@ -133,7 +128,10 @@ shaka.ui.Controls = function(player, videoContainer, video, config) {
133
128
  * @private {shaka.util.Timer}
134
129
  */
135
130
  this.timeAndSeekRangeTimer_ = new shaka.util.Timer(() => {
136
- this.updateTimeAndSeekRange_();
131
+ // Suppress timer-based updates if the controls are hidden.
132
+ if (this.isOpaque_()) {
133
+ this.updateTimeAndSeekRange_();
134
+ }
137
135
  });
138
136
 
139
137
  /** @private {?number} */
@@ -145,13 +143,12 @@ shaka.ui.Controls = function(player, videoContainer, video, config) {
145
143
  /** @private {shaka.ui.Localization} */
146
144
  this.localization_ = shaka.ui.Controls.createLocalization_();
147
145
 
148
- this.createDOM_();
149
-
150
- this.updateLocalizedStrings_();
151
-
152
146
  /** @private {shaka.util.EventManager} */
153
147
  this.eventManager_ = new shaka.util.EventManager();
154
148
 
149
+ // Configure and create the layout of the controls
150
+ this.configure(this.config_);
151
+
155
152
  this.addEventListeners_();
156
153
 
157
154
  /**
@@ -188,11 +185,6 @@ shaka.ui.Controls.prototype.destroy = async function() {
188
185
  this.eventManager_ = null;
189
186
  }
190
187
 
191
- if (this.seekTimer_) {
192
- this.seekTimer_.stop();
193
- this.seekTimer_ = null;
194
- }
195
-
196
188
  if (this.mouseStillTimer_) {
197
189
  this.mouseStillTimer_.stop();
198
190
  this.mouseStillTimer_ = null;
@@ -229,6 +221,10 @@ shaka.ui.Controls.prototype.destroy = async function() {
229
221
 
230
222
  await Promise.all(this.elements_.map((element) => element.destroy()));
231
223
  this.elements_ = [];
224
+
225
+ if (this.seekBar_) {
226
+ await this.seekBar_.destroy();
227
+ }
232
228
  };
233
229
 
234
230
 
@@ -351,6 +347,62 @@ shaka.ui.Controls.prototype.loadComplete = function() {
351
347
  };
352
348
 
353
349
 
350
+ /**
351
+ * @param {!shaka.extern.UIConfiguration} config
352
+ * @export
353
+ */
354
+ shaka.ui.Controls.prototype.configure = function(config) {
355
+ this.config_ = config;
356
+
357
+ this.castProxy_.changeReceiverId(config.castReceiverAppId);
358
+
359
+ // Deconstruct the old layout if applicable
360
+ if (this.seekBar_) {
361
+ this.seekBar_.destroy();
362
+ this.seekBar_ = null;
363
+ }
364
+
365
+ if (this.playButton_) {
366
+ this.playButton_.destroy();
367
+ this.playButton_ = null;
368
+ }
369
+
370
+ if (this.controlsContainer_) {
371
+ shaka.util.Dom.removeAllChildren(this.controlsContainer_);
372
+ this.videoContainer_.removeChild(this.spinnerContainer_);
373
+ } else {
374
+ this.addControlsContainer_();
375
+ }
376
+
377
+ // Create the new layout
378
+ this.createDOM_();
379
+
380
+ // Init the play state
381
+ this.onPlayStateChange_();
382
+
383
+ // Elements that should not propagate clicks (controls panel, menus)
384
+ const noPropagationElements = this.videoContainer_.getElementsByClassName(
385
+ 'shaka-no-propagation');
386
+ for (const element of noPropagationElements) {
387
+ const cb = (event) => event.stopPropagation();
388
+ this.eventManager_.listen(element, 'click', cb);
389
+ this.eventManager_.listen(element, 'dblclick', cb);
390
+ }
391
+
392
+ // Keep showing controls if one of those elements is hovered
393
+ const showControlsElements = this.videoContainer_.getElementsByClassName(
394
+ 'shaka-show-controls-on-mouse-over');
395
+ for (const element of showControlsElements) {
396
+ this.eventManager_.listen(element, 'mouseover', () => {
397
+ this.overrideCssShowControls_ = true;
398
+ });
399
+
400
+ this.eventManager_.listen(element, 'mouseleave', () => {
401
+ this.overrideCssShowControls_ = false;
402
+ });
403
+ }
404
+ };
405
+
354
406
  /**
355
407
  * Enable or disable the custom controls. Enabling disables native
356
408
  * browser controls.
@@ -361,16 +413,20 @@ shaka.ui.Controls.prototype.loadComplete = function() {
361
413
  shaka.ui.Controls.prototype.setEnabledShakaControls = function(enabled) {
362
414
  this.enabled_ = enabled;
363
415
  if (enabled) {
416
+ shaka.ui.Utils.setDisplay(this.controlsContainer_, true);
417
+
418
+ // Spinner lives outside of the main controls div
364
419
  shaka.ui.Utils.setDisplay(
365
- this.controlsButtonPanel_.parentElement, true);
420
+ this.spinnerContainer_, this.player_.isBuffering());
366
421
 
367
422
  // If we're hiding native controls, make sure the video element itself is
368
423
  // not tab-navigable. Our custom controls will still be tab-navigable.
369
424
  this.video_.tabIndex = -1;
370
425
  this.video_.controls = false;
371
426
  } else {
372
- shaka.ui.Utils.setDisplay(
373
- this.controlsButtonPanel_.parentElement, false);
427
+ shaka.ui.Utils.setDisplay(this.controlsContainer_, false);
428
+ // Spinner lives outside of the main controls div
429
+ shaka.ui.Utils.setDisplay(this.spinnerContainer_, false);
374
430
  }
375
431
 
376
432
  // The effects of play state changes are inhibited while showing native
@@ -379,6 +435,15 @@ shaka.ui.Controls.prototype.setEnabledShakaControls = function(enabled) {
379
435
  };
380
436
 
381
437
 
438
+ /**
439
+ * @param {boolean} seeking
440
+ * @export
441
+ */
442
+ shaka.ui.Controls.prototype.setSeeking = function(seeking) {
443
+ this.isSeeking_ = seeking;
444
+ };
445
+
446
+
382
447
  /**
383
448
  * Enable or disable native browser controls. Enabling disables shaka
384
449
  * controls.
@@ -417,6 +482,24 @@ shaka.ui.Controls.prototype.getLocalization = function() {
417
482
  return this.localization_;
418
483
  };
419
484
 
485
+ /** @export */
486
+ shaka.ui.Controls.prototype.toggleFullScreen = async function() {
487
+ if (document.fullscreenElement) {
488
+ document.exitFullscreen();
489
+ } else {
490
+ // If we are in PiP mode, leave PiP mode first.
491
+ try {
492
+ if (document.pictureInPictureElement) {
493
+ await document.exitPictureInPicture();
494
+ }
495
+ } catch (error) {
496
+ this.dispatchEvent(new shaka.util.FakeEvent('error', {
497
+ detail: error,
498
+ }));
499
+ }
500
+ await this.videoContainer_.requestFullscreen();
501
+ }
502
+ };
420
503
 
421
504
  /**
422
505
  * @return {!HTMLElement}
@@ -504,11 +587,7 @@ shaka.ui.Controls.prototype.isCastAllowed = function() {
504
587
  * @export
505
588
  */
506
589
  shaka.ui.Controls.prototype.getDisplayTime = function() {
507
- const displayTime = this.isSeeking_ ?
508
- Number(this.seekBar_.value) :
509
- Number(this.video_.currentTime);
510
-
511
- return displayTime;
590
+ return this.seekBar_ ? this.seekBar_.getValue() : this.video_.currentTime;
512
591
  };
513
592
 
514
593
 
@@ -554,67 +633,28 @@ shaka.ui.Controls.prototype.hideSettingsMenus = function() {
554
633
  };
555
634
 
556
635
 
557
- /**
558
- * @private
559
- */
560
- shaka.ui.Controls.prototype.updateLocalizedStrings_ = function() {
561
- const LocIds = shaka.ui.Locales.Ids;
562
-
563
- if (this.seekBar_) {
564
- this.seekBar_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
565
- this.localization_.resolve(LocIds.SEEK));
566
- }
567
-
568
- // Localize state-dependant labels
569
- const makePlayNotPause = this.video_.paused && !this.isSeeking_;
570
- const playButtonAriaLabelId = makePlayNotPause ? LocIds.PLAY : LocIds.PAUSE;
571
- this.playButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
572
- this.localization_.resolve(playButtonAriaLabelId));
573
- };
574
-
575
-
576
- /**
577
- * @private
578
- */
579
- shaka.ui.Controls.prototype.initOptionalElementsToNull_ = function() {
580
- // TODO: encapsulate/abstract range inputs and their containers
581
-
582
- /** @private {HTMLElement} */
583
- this.seekBarContainer_ = null;
584
-
585
- /** @private {HTMLInputElement} */
586
- this.seekBar_ = null;
587
- };
588
-
589
-
590
636
  /**
591
637
  * @private
592
638
  */
593
639
  shaka.ui.Controls.prototype.createDOM_ = function() {
594
- this.initOptionalElementsToNull_();
595
-
596
640
  this.videoContainer_.classList.add('shaka-video-container');
597
641
  this.video_.classList.add('shaka-video');
598
642
 
599
- this.addControlsContainer_();
600
-
601
- this.addPlayButton_();
643
+ if (this.config_.addBigPlayButton) {
644
+ this.addPlayButton_();
645
+ }
602
646
 
603
647
  this.addBufferingSpinner_();
604
648
 
605
649
  this.addControlsButtonPanel_();
606
650
 
607
- // Seek bar
608
- if (this.config_.addSeekBar) {
609
- this.addSeekBar_();
610
- }
611
-
612
- /** @private {!Array.<!Element>} */
613
651
  this.settingsMenus_ = Array.from(
614
652
  this.videoContainer_.getElementsByClassName('shaka-settings-menu'));
615
653
 
616
- // Settings menus need to be positioned lower, if the seekbar is absent.
617
- if (!this.seekBar_) {
654
+ if (this.config_.addSeekBar) {
655
+ this.seekBar_ = new shaka.ui.SeekBar(this.bottomControls_, this);
656
+ } else {
657
+ // Settings menus need to be positioned lower, if the seekbar is absent.
618
658
  for (let menu of this.settingsMenus_) {
619
659
  menu.classList.add('shaka-low-position');
620
660
  }
@@ -630,6 +670,14 @@ shaka.ui.Controls.prototype.addControlsContainer_ = function() {
630
670
  this.controlsContainer_ = shaka.util.Dom.createHTMLElement('div');
631
671
  this.controlsContainer_.classList.add('shaka-controls-container');
632
672
  this.videoContainer_.appendChild(this.controlsContainer_);
673
+
674
+ this.eventManager_.listen(this.controlsContainer_, 'touchstart', (e) => {
675
+ this.onContainerTouch_(e);
676
+ }, {passive: false});
677
+
678
+ this.eventManager_.listen(this.controlsContainer_, 'click', (e) => {
679
+ this.onContainerClick_(e);
680
+ });
633
681
  };
634
682
 
635
683
 
@@ -637,16 +685,13 @@ shaka.ui.Controls.prototype.addControlsContainer_ = function() {
637
685
  * @private
638
686
  */
639
687
  shaka.ui.Controls.prototype.addPlayButton_ = function() {
640
- /** @private {!HTMLElement} */
641
- this.playButtonContainer_ = shaka.util.Dom.createHTMLElement('div');
642
- this.playButtonContainer_.classList.add('shaka-play-button-container');
643
- this.controlsContainer_.appendChild(this.playButtonContainer_);
688
+ const playButtonContainer = shaka.util.Dom.createHTMLElement('div');
689
+ playButtonContainer.classList.add('shaka-play-button-container');
690
+ this.controlsContainer_.appendChild(playButtonContainer);
644
691
 
645
- /** @private {!HTMLElement} */
646
- this.playButton_ = shaka.util.Dom.createHTMLElement('button');
647
- this.playButton_.classList.add('shaka-play-button');
648
- this.playButton_.setAttribute('icon', 'play');
649
- this.playButtonContainer_.appendChild(this.playButton_);
692
+ /** @private {shaka.ui.BigPlayButton} */
693
+ this.playButton_ =
694
+ new shaka.ui.BigPlayButton(playButtonContainer, this);
650
695
  };
651
696
 
652
697
 
@@ -654,19 +699,23 @@ shaka.ui.Controls.prototype.addPlayButton_ = function() {
654
699
  * @private
655
700
  */
656
701
  shaka.ui.Controls.prototype.addBufferingSpinner_ = function() {
657
- goog.asserts.assert(this.playButtonContainer_,
658
- 'Must have play button container before spinner!');
702
+ /** @private {!HTMLElement} */
703
+ this.spinnerContainer_ = shaka.util.Dom.createHTMLElement('div');
704
+ this.spinnerContainer_.classList.add('shaka-spinner-container');
705
+ this.videoContainer_.appendChild(this.spinnerContainer_);
706
+
707
+ const spinner = shaka.util.Dom.createHTMLElement('div');
708
+ spinner.classList.add('shaka-spinner');
709
+ this.spinnerContainer_.appendChild(spinner);
659
710
 
660
711
  // Svg elements have to be created with the svg xml namespace.
661
712
  const xmlns = 'http://www.w3.org/2000/svg';
662
713
 
663
- /** @private {!HTMLElement} */
664
- this.bufferingSpinner_ =
714
+ const svg =
665
715
  /** @type {!HTMLElement} */(document.createElementNS(xmlns, 'svg'));
666
- // NOTE: SVG elements do not have a classList on IE, so use setAttribute.
667
- this.bufferingSpinner_.setAttribute('class', 'shaka-spinner-svg');
668
- this.bufferingSpinner_.setAttribute('viewBox', '0 0 30 30');
669
- this.playButton_.appendChild(this.bufferingSpinner_);
716
+ svg.setAttribute('class', 'shaka-spinner-svg');
717
+ svg.setAttribute('viewBox', '0 0 30 30');
718
+ spinner.appendChild(svg);
670
719
 
671
720
  // These coordinates are relative to the SVG viewBox above. This is distinct
672
721
  // from the actual display size in the page, since the "S" is for "Scalable."
@@ -680,7 +729,7 @@ shaka.ui.Controls.prototype.addBufferingSpinner_ = function() {
680
729
  spinnerCircle.setAttribute('fill', 'none');
681
730
  spinnerCircle.setAttribute('stroke-width', '1');
682
731
  spinnerCircle.setAttribute('stroke-miterlimit', '10');
683
- this.bufferingSpinner_.appendChild(spinnerCircle);
732
+ svg.appendChild(spinnerCircle);
684
733
  };
685
734
 
686
735
 
@@ -688,12 +737,25 @@ shaka.ui.Controls.prototype.addBufferingSpinner_ = function() {
688
737
  * @private
689
738
  */
690
739
  shaka.ui.Controls.prototype.addControlsButtonPanel_ = function() {
740
+ /** @private {!HTMLElement} */
741
+ this.bottomControls_ = shaka.util.Dom.createHTMLElement('div');
742
+ this.bottomControls_.classList.add('shaka-bottom-controls');
743
+ this.controlsContainer_.appendChild(this.bottomControls_);
744
+
691
745
  /** @private {!HTMLElement} */
692
746
  this.controlsButtonPanel_ = shaka.util.Dom.createHTMLElement('div');
693
747
  this.controlsButtonPanel_.classList.add('shaka-controls-button-panel');
694
748
  this.controlsButtonPanel_.classList.add('shaka-no-propagation');
695
749
  this.controlsButtonPanel_.classList.add('shaka-show-controls-on-mouse-over');
696
- this.controlsContainer_.appendChild(this.controlsButtonPanel_);
750
+ this.bottomControls_.appendChild(this.controlsButtonPanel_);
751
+
752
+ // Overflow menus are supposed to hide once you click elsewhere
753
+ // on the video element. The code in onContainerClick_ ensures that.
754
+ // However, clicks on controls panel don't propagate to the container,
755
+ // so we have to explicitly hide the menus onclick here.
756
+ this.eventManager_.listen(this.controlsButtonPanel_, 'click', () => {
757
+ this.hideSettingsMenus();
758
+ });
697
759
 
698
760
  // Create the elements specified by controlPanelElements
699
761
  for (let i = 0; i < this.config_.controlPanelElements.length; i++) {
@@ -710,12 +772,36 @@ shaka.ui.Controls.prototype.addControlsButtonPanel_ = function() {
710
772
 
711
773
 
712
774
  /**
775
+ * When a mobile device is rotated to landscape layout, and the video is
776
+ * loaded, make the demo app go into fullscreen.
777
+ * Similarly, exit fullscreen when the device is rotated to portrait layout.
778
+ * @private
779
+ */
780
+ shaka.ui.Controls.prototype.onScreenRotation_ = function() {
781
+ if (!this.video_ ||
782
+ this.video_.readyState == 0 ||
783
+ this.castProxy_.isCasting()) { return; }
784
+
785
+ if (screen.orientation.type.includes('landscape') &&
786
+ !document.fullscreenElement) {
787
+ this.videoContainer_.requestFullscreen();
788
+ } else if (screen.orientation.type.includes('portrait') &&
789
+ document.fullscreenElement) {
790
+ document.exitFullscreen();
791
+ }
792
+ };
793
+
794
+
795
+ /**
796
+ * Adds static event listeners. This should only add event listeners to
797
+ * things that don't change (e.g. Player). Dynamic elements (e.g. controls)
798
+ * should have their event listeners added when they are created.
713
799
  * @private
714
800
  */
715
801
  shaka.ui.Controls.prototype.addEventListeners_ = function() {
716
802
  // TODO: Convert adding event listers to the "() =>" form.
717
803
 
718
- this.player_.addEventListener('buffering', () => {
804
+ this.eventManager_.listen(this.player_, 'buffering', () => {
719
805
  this.onBufferingStateChange_();
720
806
  });
721
807
  // Set the initial state, as well.
@@ -725,117 +811,41 @@ shaka.ui.Controls.prototype.addEventListeners_ = function() {
725
811
  // for focused elements.
726
812
  this.eventManager_.listen(window, 'keydown', this.onKeyDown_.bind(this));
727
813
 
728
- this.video_.addEventListener(
814
+ this.eventManager_.listen(
815
+ this.controlsContainer_, 'dblclick', () => this.toggleFullScreen());
816
+
817
+ this.eventManager_.listen(this.video_,
729
818
  'play', this.onPlayStateChange_.bind(this));
730
- this.video_.addEventListener(
819
+ this.eventManager_.listen(this.video_,
731
820
  'pause', this.onPlayStateChange_.bind(this));
732
821
 
733
822
  // Since videos go into a paused state at the end, Chrome and Edge both fire
734
823
  // the 'pause' event when a video ends. IE 11 only fires the 'ended' event.
735
- this.video_.addEventListener(
824
+ this.eventManager_.listen(this.video_,
736
825
  'ended', this.onPlayStateChange_.bind(this));
737
826
 
738
- if (this.seekBar_) {
739
- this.seekBar_.addEventListener(
740
- 'mousedown', this.onSeekStart_.bind(this));
741
- this.seekBar_.addEventListener(
742
- 'touchstart', this.onSeekStart_.bind(this), {passive: true});
743
- this.seekBar_.addEventListener(
744
- 'input', this.onSeekInput_.bind(this));
745
- this.seekBar_.addEventListener(
746
- 'touchend', this.onSeekEnd_.bind(this));
747
- this.seekBar_.addEventListener(
748
- 'mouseup', this.onSeekEnd_.bind(this));
749
- }
750
-
751
- this.controlsContainer_.addEventListener(
752
- 'touchstart', this.onContainerTouch_.bind(this), {passive: false});
753
- this.controlsContainer_.addEventListener(
754
- 'click', this.onContainerClick_.bind(this));
755
-
756
- // Elements that should not propagate clicks (controls panel, menus)
757
- const noPropagationElements = this.videoContainer_.getElementsByClassName(
758
- 'shaka-no-propagation');
759
- for (let i = 0; i < noPropagationElements.length; i++) {
760
- let element = noPropagationElements[i];
761
- element.addEventListener(
762
- 'click', function(event) { event.stopPropagation(); });
763
- }
764
-
765
- // Keep showing controls if one of those elements is hovered
766
- let showControlsElements = this.videoContainer_.getElementsByClassName(
767
- 'shaka-show-controls-on-mouse-over');
768
- for (let i = 0; i < showControlsElements.length; i++) {
769
- let element = showControlsElements[i];
770
- element.addEventListener(
771
- 'mouseover', () => {
772
- this.overrideCssShowControls_ = true;
773
- });
774
-
775
- element.addEventListener(
776
- 'mouseleave', () => {
777
- this.overrideCssShowControls_ = false;
778
- });
779
- }
780
-
781
- this.videoContainer_.addEventListener(
827
+ this.eventManager_.listen(this.videoContainer_,
782
828
  'mousemove', this.onMouseMove_.bind(this));
783
- this.videoContainer_.addEventListener(
829
+ this.eventManager_.listen(this.videoContainer_,
784
830
  'touchmove', this.onMouseMove_.bind(this), {passive: true});
785
- this.videoContainer_.addEventListener(
831
+ this.eventManager_.listen(this.videoContainer_,
786
832
  'touchend', this.onMouseMove_.bind(this), {passive: true});
787
- this.videoContainer_.addEventListener(
833
+ this.eventManager_.listen(this.videoContainer_,
788
834
  'mouseleave', this.onMouseLeave_.bind(this));
789
835
 
790
- // Overflow menus are supposed to hide once you click elsewhere
791
- // on the video element. The code in onContainerClick_ ensures that.
792
- // However, clicks on controls panel don't propagate to the container,
793
- // so we have to explicitly hide the menus onclick here.
794
- this.controlsButtonPanel_.addEventListener('click', () => {
795
- this.hideSettingsMenusTimer_.tickNow();
796
- });
797
-
798
- this.castProxy_.addEventListener(
836
+ this.eventManager_.listen(this.castProxy_,
799
837
  'caststatuschanged', (e) => {
800
838
  this.onCastStatusChange_(e);
801
839
  });
802
840
 
803
- this.videoContainer_.addEventListener('keyup', this.onKeyUp_.bind(this));
804
-
805
- this.localization_.addEventListener(
806
- shaka.ui.Localization.LOCALE_UPDATED,
807
- (e) => this.updateLocalizedStrings_());
808
-
809
- this.localization_.addEventListener(
810
- shaka.ui.Localization.LOCALE_CHANGED,
811
- (e) => this.updateLocalizedStrings_());
812
- };
813
-
841
+ this.eventManager_.listen(this.videoContainer_,
842
+ 'keyup', this.onKeyUp_.bind(this));
814
843
 
815
- /**
816
- * @private
817
- */
818
- shaka.ui.Controls.prototype.addSeekBar_ = function() {
819
- // This container is to support IE 11. See detailed notes in
820
- // less/range_elements.less for a complete explanation.
821
- // TODO: Factor this into a range-element component.
822
- this.seekBarContainer_ = shaka.util.Dom.createHTMLElement('div');
823
- this.seekBarContainer_.classList.add('shaka-seek-bar-container');
824
-
825
- this.seekBar_ =
826
- /** @type {!HTMLInputElement} */ (document.createElement('input'));
827
- this.seekBar_.classList.add('shaka-seek-bar');
828
- this.seekBar_.type = 'range';
829
- // NOTE: step=any causes keyboard nav problems on IE 11.
830
- this.seekBar_.setAttribute('step', 'any');
831
- this.seekBar_.setAttribute('min', '0');
832
- this.seekBar_.setAttribute('max', '1');
833
- this.seekBar_.value = '0';
834
- this.seekBar_.classList.add('shaka-no-propagation');
835
- this.seekBar_.classList.add('shaka-show-controls-on-mouse-over');
836
-
837
- this.seekBarContainer_.appendChild(this.seekBar_);
838
- this.controlsContainer_.appendChild(this.seekBarContainer_);
844
+ if (screen.orientation) {
845
+ this.eventManager_.listen(screen.orientation, 'change', () => {
846
+ this.onScreenRotation_();
847
+ });
848
+ }
839
849
  };
840
850
 
841
851
 
@@ -873,8 +883,15 @@ shaka.ui.Controls.prototype.onMouseMove_ = function(event) {
873
883
  // Make sure we are not about to hide the settings menus and then force them
874
884
  // open.
875
885
  this.hideSettingsMenusTimer_.stop();
876
- this.setControlsOpacity_(shaka.ui.Enums.Opacity.OPAQUE);
877
- this.updateTimeAndSeekRange_();
886
+
887
+ if (!this.isOpaque_()) {
888
+ // Only update the time and seek range on mouse movement if it's the very
889
+ // first movement and we're about to show the controls. Otherwise the
890
+ // seek bar will be updated mich more rapidly during mouse movement. Do
891
+ // this right before making it visible.
892
+ this.updateTimeAndSeekRange_();
893
+ this.setControlsOpacity_(shaka.ui.Enums.Opacity.OPAQUE);
894
+ }
878
895
 
879
896
  // Hide the cursor when the mouse stops moving.
880
897
  // Only applies while the cursor is over the video container.
@@ -1007,63 +1024,6 @@ shaka.ui.Controls.prototype.onPlayStateChange_ = function() {
1007
1024
  if (this.video_.ended && !this.video_.paused) {
1008
1025
  this.video_.pause();
1009
1026
  }
1010
-
1011
- // Video is paused during seek, so don't show the play arrow while seeking:
1012
- if (this.enabled_ && this.video_.paused && !this.isSeeking_) {
1013
- this.playButton_.setAttribute('icon', 'play');
1014
- this.playButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
1015
- this.localization_.resolve(shaka.ui.Locales.Ids.PLAY));
1016
- } else {
1017
- this.playButton_.setAttribute('icon', 'pause');
1018
- this.playButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
1019
- this.localization_.resolve(shaka.ui.Locales.Ids.PAUSE));
1020
- }
1021
- };
1022
-
1023
-
1024
- /** @private */
1025
- shaka.ui.Controls.prototype.onSeekStart_ = function() {
1026
- if (!this.enabled_) return;
1027
-
1028
- this.isSeeking_ = true;
1029
- this.video_.pause();
1030
- };
1031
-
1032
-
1033
- /** @private */
1034
- shaka.ui.Controls.prototype.onSeekInput_ = function() {
1035
- if (!this.enabled_) return;
1036
-
1037
- if (!this.video_.duration) {
1038
- // Can't seek yet. Ignore.
1039
- return;
1040
- }
1041
-
1042
- // Update the UI right away.
1043
- this.updateTimeAndSeekRange_();
1044
-
1045
- // We want to wait until the user has stopped moving the seek bar for a
1046
- // little bit to avoid the number of times we ask the player to seek.
1047
- //
1048
- // To do this, we will start a timer that will fire in a little bit, but if
1049
- // we see another seek bar change, we will cancel that timer and re-start it.
1050
- //
1051
- // Calling |start| on an already pending timer will cancel the old request
1052
- // and start the new one.
1053
- this.seekTimer_.tickAfter(/* seconds= */ 0.125);
1054
- };
1055
-
1056
-
1057
- /** @private */
1058
- shaka.ui.Controls.prototype.onSeekEnd_ = function() {
1059
- if (!this.enabled_) return;
1060
-
1061
- // They just let go of the seek bar, so cancel the timer and manually
1062
- // call the event so that we can respond immediately.
1063
- this.seekTimer_.tickNow();
1064
-
1065
- this.isSeeking_ = false;
1066
- this.video_.play();
1067
1027
  };
1068
1028
 
1069
1029
 
@@ -1128,15 +1088,12 @@ shaka.ui.Controls.prototype.onKeyUp_ = function(event) {
1128
1088
  * @private
1129
1089
  */
1130
1090
  shaka.ui.Controls.prototype.onBufferingStateChange_ = function() {
1131
- // Don't use setDisplay_ here, since the SVG spinner doesn't have classList
1132
- // on IE.
1133
- if (this.player_.isBuffering()) {
1134
- this.bufferingSpinner_.setAttribute(
1135
- 'class', 'shaka-spinner-svg');
1136
- } else {
1137
- this.bufferingSpinner_.setAttribute(
1138
- 'class', 'shaka-spinner-svg shaka-hidden');
1091
+ if (!this.enabled_) {
1092
+ return;
1139
1093
  }
1094
+
1095
+ shaka.ui.Utils.setDisplay(
1096
+ this.spinnerContainer_, this.player_.isBuffering());
1140
1097
  };
1141
1098
 
1142
1099
 
@@ -1163,7 +1120,10 @@ shaka.ui.Controls.prototype.isOpaque_ = function() {
1163
1120
  */
1164
1121
  shaka.ui.Controls.prototype.seek_ = function(currentTime, event) {
1165
1122
  this.video_.currentTime = currentTime;
1166
- this.updateTimeAndSeekRange_();
1123
+ if (this.isOpaque_()) {
1124
+ // Only update the time and esek range if it's visible.
1125
+ this.updateTimeAndSeekRange_();
1126
+ }
1167
1127
  };
1168
1128
 
1169
1129
 
@@ -1172,87 +1132,17 @@ shaka.ui.Controls.prototype.seek_ = function(currentTime, event) {
1172
1132
  * @private
1173
1133
  */
1174
1134
  shaka.ui.Controls.prototype.updateTimeAndSeekRange_ = function() {
1175
- // Suppress updates if the controls are hidden.
1176
- if (!this.isOpaque_()) {
1177
- return;
1178
- }
1179
-
1180
- const Constants = shaka.ui.Constants;
1181
- let displayTime = this.isSeeking_ ?
1182
- Number(this.seekBar_.value) :
1183
- Number(this.video_.currentTime);
1184
- let bufferedLength = this.video_.buffered.length;
1185
- let bufferedStart = bufferedLength ? this.video_.buffered.start(0) : 0;
1186
- let bufferedEnd =
1187
- bufferedLength ? this.video_.buffered.end(bufferedLength - 1) : 0;
1188
- let seekRange = this.player_.seekRange();
1189
- let seekRangeSize = seekRange.end - seekRange.start;
1190
-
1191
1135
  if (this.seekBar_) {
1192
- this.seekBar_.min = seekRange.start;
1193
- this.seekBar_.max = seekRange.end;
1194
- }
1195
-
1196
- if (this.player_.isLive()) {
1197
- // The amount of time we are behind the live edge.
1198
- let behindLive = Math.floor(seekRange.end - displayTime);
1199
- displayTime = Math.max(0, behindLive);
1200
-
1201
- if (!this.isSeeking_ && this.seekBar_) {
1202
- this.seekBar_.value = seekRange.end - displayTime;
1203
- }
1204
- } else {
1205
- if (!this.isSeeking_ && this.seekBar_) {
1206
- this.seekBar_.value = displayTime;
1207
- }
1208
- }
1209
-
1210
- if (this.seekBar_) {
1211
- // Hide seekbar seek window is very small.
1212
- const seekRange = this.player_.seekRange();
1213
- const seekWindow = seekRange.end - seekRange.start;
1214
- if (seekWindow < Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
1215
- shaka.ui.Utils.setDisplay(this.seekBarContainer_, false);
1136
+ this.seekBar_.setValue(this.video_.currentTime);
1137
+ this.seekBar_.update();
1138
+ if (this.seekBar_.isShowing()) {
1216
1139
  for (let menu of this.settingsMenus_) {
1217
- menu.classList.add('shaka-low-position');
1140
+ menu.classList.remove('shaka-low-position');
1218
1141
  }
1219
1142
  } else {
1220
- shaka.ui.Utils.setDisplay(this.seekBarContainer_, true);
1221
1143
  for (let menu of this.settingsMenus_) {
1222
- menu.classList.remove('shaka-low-position');
1223
- }
1224
-
1225
- let gradient = ['to right'];
1226
- if (bufferedLength == 0) {
1227
- gradient.push('#000 0%');
1228
- } else {
1229
- const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
1230
- const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
1231
-
1232
- const bufferStartDistance = clampedBufferStart - seekRange.start;
1233
- const bufferEndDistance = clampedBufferEnd - seekRange.start;
1234
- const playheadDistance = displayTime - seekRange.start;
1235
-
1236
- // NOTE: the fallback to zero eliminates NaN.
1237
- const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
1238
- const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
1239
- const playheadFraction = (playheadDistance / seekRangeSize) || 0;
1240
-
1241
- gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
1242
- (bufferStartFraction * 100) + '%');
1243
- gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
1244
- (bufferStartFraction * 100) + '%');
1245
- gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
1246
- (playheadFraction * 100) + '%');
1247
- gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
1248
- (playheadFraction * 100) + '%');
1249
- gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
1250
- (bufferEndFraction * 100) + '%');
1251
- gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
1252
- (bufferEndFraction * 100) + '%');
1144
+ menu.classList.add('shaka-low-position');
1253
1145
  }
1254
- this.seekBarContainer_.style.background =
1255
- 'linear-gradient(' + gradient.join(',') + ')';
1256
1146
  }
1257
1147
  }
1258
1148
 
@@ -1353,8 +1243,9 @@ shaka.ui.Controls.prototype.keepFocusInMenu_ = function(event) {
1353
1243
 
1354
1244
 
1355
1245
  /**
1356
- * Removes class for keyboard navigation if mouse navigation
1357
- * is active.
1246
+ * For keyboard navigation, we use blue borders to highlight the active
1247
+ * element. If we detect that a mouse is being used, remove the blue border
1248
+ * from the active element.
1358
1249
  * @private
1359
1250
  */
1360
1251
  shaka.ui.Controls.prototype.onMouseDown_ = function() {