Source: ui/pip_button.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.PipButton');
  7. goog.require('shaka.ui.Constants');
  8. goog.require('shaka.ui.Element');
  9. goog.require('shaka.ui.Enums');
  10. goog.require('shaka.ui.Locales');
  11. goog.require('shaka.ui.Localization');
  12. goog.require('shaka.ui.OverflowMenu');
  13. goog.require('shaka.ui.Utils');
  14. goog.require('shaka.util.Dom');
  15. goog.require('shaka.util.FakeEvent');
  16. goog.requireType('shaka.ui.Controls');
  17. /**
  18. * @extends {shaka.ui.Element}
  19. * @final
  20. * @export
  21. */
  22. shaka.ui.PipButton = class extends shaka.ui.Element {
  23. /**
  24. * @param {!HTMLElement} parent
  25. * @param {!shaka.ui.Controls} controls
  26. */
  27. constructor(parent, controls) {
  28. super(parent, controls);
  29. /** @private {HTMLMediaElement} */
  30. this.localVideo_ = this.controls.getLocalVideo();
  31. const LocIds = shaka.ui.Locales.Ids;
  32. /** @private {!HTMLButtonElement} */
  33. this.pipButton_ = shaka.util.Dom.createButton();
  34. this.pipButton_.classList.add('shaka-pip-button');
  35. /** @private {!HTMLElement} */
  36. this.pipIcon_ = shaka.util.Dom.createHTMLElement('i');
  37. this.pipIcon_.classList.add('material-icons-round');
  38. this.pipIcon_.textContent = shaka.ui.Enums.MaterialDesignIcons.PIP;
  39. this.pipButton_.appendChild(this.pipIcon_);
  40. const label = shaka.util.Dom.createHTMLElement('label');
  41. label.classList.add('shaka-overflow-button-label');
  42. this.pipNameSpan_ = shaka.util.Dom.createHTMLElement('span');
  43. this.pipNameSpan_.textContent =
  44. this.localization.resolve(LocIds.PICTURE_IN_PICTURE);
  45. label.appendChild(this.pipNameSpan_);
  46. /** @private {!HTMLElement} */
  47. this.currentPipState_ = shaka.util.Dom.createHTMLElement('span');
  48. this.currentPipState_.classList.add('shaka-current-selection-span');
  49. label.appendChild(this.currentPipState_);
  50. this.pipButton_.appendChild(label);
  51. this.updateLocalizedStrings_();
  52. this.parent.appendChild(this.pipButton_);
  53. // Don't display the button if PiP is not supported or not allowed.
  54. // TODO: Can this ever change? Is it worth creating the button if the below
  55. // condition is true?
  56. if (!this.isPipAllowed_()) {
  57. shaka.ui.Utils.setDisplay(this.pipButton_, false);
  58. }
  59. this.eventManager.listen(
  60. this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
  61. this.updateLocalizedStrings_();
  62. });
  63. this.eventManager.listen(
  64. this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
  65. this.updateLocalizedStrings_();
  66. });
  67. this.eventManager.listen(this.pipButton_, 'click', () => {
  68. this.onPipClick_();
  69. });
  70. this.eventManager.listen(this.localVideo_, 'enterpictureinpicture', () => {
  71. this.onEnterPictureInPicture_();
  72. });
  73. this.eventManager.listen(this.localVideo_, 'leavepictureinpicture', () => {
  74. this.onLeavePictureInPicture_();
  75. });
  76. this.eventManager.listen(this.controls, 'caststatuschanged', (e) => {
  77. this.onCastStatusChange_(e);
  78. });
  79. this.eventManager.listen(this.player, 'trackschanged', () => {
  80. this.onTracksChanged_();
  81. });
  82. }
  83. /**
  84. * @return {boolean}
  85. * @private
  86. */
  87. isPipAllowed_() {
  88. return document.pictureInPictureEnabled &&
  89. !this.video.disablePictureInPicture;
  90. }
  91. /**
  92. * @return {!Promise}
  93. * @private
  94. */
  95. async onPipClick_() {
  96. try {
  97. if (!document.pictureInPictureElement) {
  98. // If you were fullscreen, leave fullscreen first.
  99. if (document.fullscreenElement) {
  100. document.exitFullscreen();
  101. }
  102. await this.video.requestPictureInPicture();
  103. } else {
  104. await document.exitPictureInPicture();
  105. }
  106. } catch (error) {
  107. this.controls.dispatchEvent(new shaka.util.FakeEvent('error', {
  108. detail: error,
  109. }));
  110. }
  111. }
  112. /** @private */
  113. onEnterPictureInPicture_() {
  114. const LocIds = shaka.ui.Locales.Ids;
  115. this.pipIcon_.textContent = shaka.ui.Enums.MaterialDesignIcons.EXIT_PIP;
  116. this.pipButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
  117. this.localization.resolve(LocIds.EXIT_PICTURE_IN_PICTURE));
  118. this.currentPipState_.textContent =
  119. this.localization.resolve(LocIds.ON);
  120. }
  121. /** @private */
  122. onLeavePictureInPicture_() {
  123. const LocIds = shaka.ui.Locales.Ids;
  124. this.pipIcon_.textContent = shaka.ui.Enums.MaterialDesignIcons.PIP;
  125. this.pipButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
  126. this.localization.resolve(LocIds.ENTER_PICTURE_IN_PICTURE));
  127. this.currentPipState_.textContent =
  128. this.localization.resolve(LocIds.OFF);
  129. }
  130. /**
  131. * @private
  132. */
  133. updateLocalizedStrings_() {
  134. const LocIds = shaka.ui.Locales.Ids;
  135. this.pipNameSpan_.textContent =
  136. this.localization.resolve(LocIds.PICTURE_IN_PICTURE);
  137. const ariaLabel = document.pictureInPictureElement ?
  138. LocIds.EXIT_PICTURE_IN_PICTURE :
  139. LocIds.ENTER_PICTURE_IN_PICTURE;
  140. this.pipButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
  141. this.localization.resolve(ariaLabel));
  142. const currentPipState = document.pictureInPictureElement ?
  143. LocIds.ON : LocIds.OFF;
  144. this.currentPipState_.textContent =
  145. this.localization.resolve(currentPipState);
  146. }
  147. /**
  148. * @param {Event} e
  149. * @private
  150. */
  151. onCastStatusChange_(e) {
  152. const isCasting = e['newStatus'];
  153. if (isCasting) {
  154. // Picture-in-picture is not applicable if we're casting
  155. if (this.isPipAllowed_()) {
  156. shaka.ui.Utils.setDisplay(this.pipButton_, false);
  157. }
  158. } else {
  159. if (this.isPipAllowed_()) {
  160. shaka.ui.Utils.setDisplay(this.pipButton_, true);
  161. }
  162. }
  163. }
  164. /**
  165. * Display the picture-in-picture button only when the content contains video.
  166. * If it's displaying in picture-in-picture mode, and an audio only content is
  167. * loaded, exit the picture-in-picture display.
  168. * @return {!Promise}
  169. * @private
  170. */
  171. async onTracksChanged_() {
  172. if (!this.isPipAllowed_()) {
  173. shaka.ui.Utils.setDisplay(this.pipButton_, false);
  174. } else if (this.player && this.player.isAudioOnly()) {
  175. shaka.ui.Utils.setDisplay(this.pipButton_, false);
  176. if (document.pictureInPictureElement) {
  177. await document.exitPictureInPicture();
  178. }
  179. } else {
  180. shaka.ui.Utils.setDisplay(this.pipButton_, true);
  181. }
  182. }
  183. };
  184. /**
  185. * @implements {shaka.extern.IUIElement.Factory}
  186. * @final
  187. */
  188. shaka.ui.PipButton.Factory = class {
  189. /** @override */
  190. create(rootElement, controls) {
  191. return new shaka.ui.PipButton(rootElement, controls);
  192. }
  193. };
  194. shaka.ui.OverflowMenu.registerElement(
  195. 'picture_in_picture', new shaka.ui.PipButton.Factory());