Source: ui/hidden_seek_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.HiddenSeekButton');
  7. goog.require('shaka.ui.Element');
  8. goog.require('shaka.util.Timer');
  9. goog.require('shaka.util.Dom');
  10. goog.requireType('shaka.ui.Controls');
  11. /**
  12. * @extends {shaka.ui.Element}
  13. * @export
  14. */
  15. shaka.ui.HiddenSeekButton = class extends shaka.ui.Element {
  16. /**
  17. * @param {!HTMLElement} parent
  18. * @param {!shaka.ui.Controls} controls
  19. */
  20. constructor(parent, controls) {
  21. super(parent, controls);
  22. /** @private {?number} */
  23. this.lastTouchEventTimeSet_ = null;
  24. /** @private {boolean} */
  25. this.triggeredTouchValid_ = false;
  26. /**
  27. * Keeps track of whether the user has moved enough
  28. * to be considered scrolling.
  29. * @private {boolean}
  30. */
  31. this.hasMoved_ = false;
  32. /**
  33. * Touch-start coordinates for detecting scroll distance.
  34. * @private {?number}
  35. */
  36. this.touchStartX_ = null;
  37. /** @private {?number} */
  38. this.touchStartY_ = null;
  39. /**
  40. * Timer used to hide the seek button container. In the timer’s callback,
  41. * if the seek value is still 0s, we interpret it as a single tap
  42. * (play/pause). If not, we perform the seek.
  43. * @private {shaka.util.Timer}
  44. */
  45. this.hideSeekButtonContainerTimer_ = new shaka.util.Timer(() => {
  46. const seekSeconds = parseInt(this.seekValue_.textContent, 10);
  47. if (seekSeconds === 0) {
  48. this.controls.onContainerClick(/* fromTouchEvent= */ true);
  49. }
  50. this.hideSeekButtonContainer_();
  51. });
  52. /** @protected {!HTMLElement} */
  53. this.seekContainer = shaka.util.Dom.createHTMLElement('div');
  54. this.seekContainer.classList.add('shaka-no-propagation');
  55. this.parent.appendChild(this.seekContainer);
  56. /** @private {!HTMLElement} */
  57. this.seekValue_ = shaka.util.Dom.createHTMLElement('span');
  58. this.seekValue_.textContent = '0s';
  59. this.seekContainer.appendChild(this.seekValue_);
  60. /** @protected {!HTMLElement} */
  61. this.seekIcon = shaka.util.Dom.createHTMLElement('span');
  62. this.seekIcon.classList.add(
  63. 'shaka-forward-rewind-container-icon');
  64. this.seekContainer.appendChild(this.seekIcon);
  65. /** @protected {boolean} */
  66. this.isRewind = false;
  67. // ---------------------------------------------------------------
  68. // TOUCH EVENT LISTENERS for SCROLL vs. TAP DETECTION
  69. // ---------------------------------------------------------------
  70. this.eventManager.listen(this.seekContainer, 'touchstart', (e) => {
  71. const event = /** @type {!TouchEvent} */(e);
  72. this.onTouchStart_(event);
  73. });
  74. this.eventManager.listen(this.seekContainer, 'touchmove', (e) => {
  75. const event = /** @type {!TouchEvent} */(e);
  76. this.onTouchMove_(event);
  77. });
  78. this.eventManager.listen(this.seekContainer, 'touchend', (e) => {
  79. const event = /** @type {!TouchEvent} */(e);
  80. this.onTouchEnd_(event);
  81. });
  82. }
  83. /**
  84. * Called when the user starts touching the screen.
  85. * We record the initial touch coordinates for scroll detection.
  86. * @param {!TouchEvent} event
  87. * @private
  88. */
  89. onTouchStart_(event) {
  90. // Only proceed if controls are visible.
  91. if (!this.controls.isOpaque()) {
  92. return;
  93. }
  94. // If multiple touches, handle or ignore as needed. Here, we assume
  95. // single-touch.
  96. if (event.touches.length > 0) {
  97. this.touchStartX_ = event.touches[0].clientX;
  98. this.touchStartY_ = event.touches[0].clientY;
  99. }
  100. this.hasMoved_ = false;
  101. }
  102. /**
  103. * Called when the user moves the finger on the screen.
  104. * If the movement exceeds the scroll threshold, we mark this as scrolling.
  105. * @param {!TouchEvent} event
  106. * @private
  107. */
  108. onTouchMove_(event) {
  109. if (event.touches.length > 0 &&
  110. this.touchStartX_ != null &&
  111. this.touchStartY_ != null) {
  112. const dx = event.touches[0].clientX - this.touchStartX_;
  113. const dy = event.touches[0].clientY - this.touchStartY_;
  114. const distance = Math.sqrt(dx * dx + dy * dy);
  115. if (distance > shaka.ui.HiddenSeekButton.SCROLL_THRESHOLD_) {
  116. this.hasMoved_ = true;
  117. }
  118. }
  119. }
  120. /**
  121. * Called when the user lifts the finger from the screen.
  122. * If we haven't moved beyond the threshold, treat it as a tap.
  123. * @param {!TouchEvent} event
  124. * @private
  125. */
  126. onTouchEnd_(event) {
  127. // Only proceed if controls are visible.
  128. if (!this.controls.isOpaque()) {
  129. return;
  130. }
  131. // If user scrolled, don't handle as a tap.
  132. if (this.hasMoved_) {
  133. return;
  134. }
  135. // If any settings menus are open, this tap closes them instead of toggling
  136. // play/seek.
  137. if (this.controls.anySettingsMenusAreOpen()) {
  138. event.preventDefault();
  139. this.controls.hideSettingsMenus();
  140. return;
  141. }
  142. // Normal tap logic (single vs double tap).
  143. if (this.controls.getConfig().tapSeekDistance > 0) {
  144. event.preventDefault();
  145. this.onSeekButtonClick_();
  146. }
  147. }
  148. /**
  149. * Determines whether this tap is a single tap (leading to play/pause)
  150. * or a double tap (leading to a seek). We use a 500 ms window.
  151. * @private
  152. */
  153. onSeekButtonClick_() {
  154. const tapSeekDistance = this.controls.getConfig().tapSeekDistance;
  155. const doubleTapWindow = shaka.ui.HiddenSeekButton.DOUBLE_TAP_WINDOW_;
  156. if (!this.triggeredTouchValid_) {
  157. // First tap: start our 500 ms "double-tap" timer.
  158. this.triggeredTouchValid_ = true;
  159. this.lastTouchEventTimeSet_ = Date.now();
  160. this.hideSeekButtonContainerTimer_.tickAfter(doubleTapWindow);
  161. } else if ((this.lastTouchEventTimeSet_ +
  162. doubleTapWindow * 1000) > Date.now()) {
  163. // Second tap arrived in time — interpret as a double tap to seek.
  164. this.hideSeekButtonContainerTimer_.stop();
  165. this.lastTouchEventTimeSet_ = Date.now();
  166. let position = parseInt(this.seekValue_.textContent, 10);
  167. if (this.isRewind) {
  168. position -= tapSeekDistance;
  169. } else {
  170. position += tapSeekDistance;
  171. }
  172. this.seekValue_.textContent = position.toString() + 's';
  173. this.seekContainer.style.opacity = '1';
  174. // Restart timer if user might tap again (triple tap).
  175. this.hideSeekButtonContainerTimer_.tickAfter(doubleTapWindow);
  176. }
  177. }
  178. /**
  179. * If the seek value is zero, interpret it as a single tap (play/pause).
  180. * Otherwise, apply the seek and reset.
  181. * @private
  182. */
  183. hideSeekButtonContainer_() {
  184. const seekSeconds = parseInt(this.seekValue_.textContent, 10);
  185. if (seekSeconds !== 0) {
  186. // Perform the seek.
  187. this.video.currentTime = this.controls.getDisplayTime() + seekSeconds;
  188. }
  189. // Hide and reset.
  190. this.seekContainer.style.opacity = '0';
  191. this.triggeredTouchValid_ = false;
  192. this.seekValue_.textContent = '0s';
  193. }
  194. };
  195. /**
  196. * The amount of time, in seconds, to double-tap detection.
  197. *
  198. * @const {number}
  199. */
  200. shaka.ui.HiddenSeekButton.DOUBLE_TAP_WINDOW_ = 0.5;
  201. /**
  202. * Minimum distance (px) the finger must move during touch to consider it a
  203. * scroll rather than a tap.
  204. *
  205. * @const {number}
  206. */
  207. shaka.ui.HiddenSeekButton.SCROLL_THRESHOLD_ = 10;